htmlgraph 0.27.7__py3-none-any.whl → 0.28.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. htmlgraph/__init__.py +1 -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 +135 -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/static/presence-widget-demo.html +785 -0
  13. htmlgraph/api/sync_routes.py +184 -0
  14. htmlgraph/api/templates/partials/agents.html +308 -80
  15. htmlgraph/api/websocket.py +112 -37
  16. htmlgraph/broadcast_integration.py +227 -0
  17. htmlgraph/cli_commands/sync.py +207 -0
  18. htmlgraph/db/schema.py +226 -0
  19. htmlgraph/hooks/event_tracker.py +53 -2
  20. htmlgraph/models.py +1 -0
  21. htmlgraph/reactive_integration.py +148 -0
  22. htmlgraph/session_manager.py +7 -0
  23. htmlgraph/sync/__init__.py +21 -0
  24. htmlgraph/sync/git_sync.py +458 -0
  25. {htmlgraph-0.27.7.dist-info → htmlgraph-0.28.1.dist-info}/METADATA +1 -1
  26. {htmlgraph-0.27.7.dist-info → htmlgraph-0.28.1.dist-info}/RECORD +32 -19
  27. htmlgraph/dashboard.html +0 -6592
  28. htmlgraph-0.27.7.data/data/htmlgraph/dashboard.html +0 -6592
  29. {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/styles.css +0 -0
  30. {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  31. {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  32. {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  33. {htmlgraph-0.27.7.dist-info → htmlgraph-0.28.1.dist-info}/WHEEL +0 -0
  34. {htmlgraph-0.27.7.dist-info → htmlgraph-0.28.1.dist-info}/entry_points.txt +0 -0
@@ -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.7
3
+ Version: 0.28.1
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=O3v_6OP4BONmloso2OFOPCxHy3DvAP4UcTNCvxXcwKs,6595
1
+ htmlgraph/__init__.py,sha256=Wqw4ewsvCTTKQEbFjt13h2ZT6RZUr-n1B4V5nwsB0dk,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,11 +7,11 @@ 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
13
14
  htmlgraph/converter.py,sha256=W9uNGo-FhFRsuxwivQeSRd6nwQ9oWVhkEDyUdS83BJQ,24704
14
- htmlgraph/dashboard.html,sha256=MUT6SaYnazoyDcvHz5hN1omYswyIoUfeoZLf2M_iblo,251268
15
15
  htmlgraph/dashboard.html.backup,sha256=MUT6SaYnazoyDcvHz5hN1omYswyIoUfeoZLf2M_iblo,251268
16
16
  htmlgraph/dashboard.html.bak,sha256=cDDMzwxkoDFFOd8V0VDCa_yigDYTvpPrP7RMoiCJ5Zo,276374
17
17
  htmlgraph/dashboard.html.bak2,sha256=Gm-4u_HV9WTQubgdFX5S5itW0u2f94N0F1R44UVjBRw,279532
@@ -33,7 +33,7 @@ htmlgraph/ids.py,sha256=c58T1pg4QFNFKb6ZjR9mmgAhZz4ImtBQEeKAh_no5gs,8390
33
33
  htmlgraph/index.d.ts,sha256=7dvExfA16g1z5Kut8xyHnSUfZ6wiUUwWNy6R7WKiwas,6922
34
34
  htmlgraph/learning.py,sha256=A3LU7VeK129sy5C0xjgVtZNI3dS6xEh9ow3GBnoq2JQ,28567
35
35
  htmlgraph/mcp_server.py,sha256=Isha3F2Jk_YLsbchtr2wusmEy6LDrMH-3zF0jzvJmFk,24034
36
- htmlgraph/models.py,sha256=rE1Arbb4CoC5HHKx_JXxtKfneH3rQVjOUVGrz-s1iMU,87352
36
+ htmlgraph/models.py,sha256=NjsoMDlipqD_7A8uXf_hGWggQf1nyOwR86B8xWmnMU4,87420
37
37
  htmlgraph/orchestration.md,sha256=7oCum1RtlkGfHjh9YQ1h24flPbHrEIKiSNjnt61CuG4,17874
38
38
  htmlgraph/orchestrator-system-prompt-optimized.txt,sha256=8ekZ19YWJE9npaAaAGTkTMfseOlA2rliGYc_ad2HaRc,31191
39
39
  htmlgraph/orchestrator.py,sha256=vF7ovxsb2rIkQs_ifX-lKp7UJO9CCUq4ZQNOdNhdwJM,19698
@@ -49,6 +49,7 @@ htmlgraph/pydantic_models.py,sha256=4vGAp2n59Hf8v5mZG-Tx2vOSm3lcE_PypBprcejJUXE,
49
49
  htmlgraph/quality_gates.py,sha256=ovPrjGgAwK5MjMzEIda4a6M5_S-yGiRJYy8KhzdyTZ0,11686
50
50
  htmlgraph/query_builder.py,sha256=GBrjf_6Qoq98JxJr8cYeAMV4QpdmrfFyadJpDjH4clM,18093
51
51
  htmlgraph/query_composer.py,sha256=ilasmZBc8_mLS1iq45Kcw2Ig1zCN04-r1HVKr8f9YP0,17332
52
+ htmlgraph/reactive_integration.py,sha256=7Vf7AqPdp-Oe-rzBvwSCV_HWCb5DvaujQRzP4oqd7ag,4113
52
53
  htmlgraph/reflection.py,sha256=agNfHsI1ynayTG3kgt5fiQj30LkG9b-v3wtnDsu4-Gk,15663
53
54
  htmlgraph/refs.py,sha256=jgsPWRTfPDxhH_dr4EeYdU1ly-e3HpuHgAPnMXMbaF4,10011
54
55
  htmlgraph/repo_hash.py,sha256=wYQlgYf0W1LfU95PA6vViA-MlUxA6ry_xDh7kyCB4v4,14888
@@ -56,7 +57,7 @@ htmlgraph/routing.py,sha256=QYDY6bzYPmv6kocAXCqguB1cazN0i_xTo9EVCO3fO2Y,8803
56
57
  htmlgraph/server.py,sha256=BZcUM9sfGQzS0PyFOvl5_f4MP9-8C4E8tp77VcanwLM,56211
57
58
  htmlgraph/session_context.py,sha256=4XsywYg4-J7ct9B-z3mnbkIUvyD_K389Y73yh5AxBWQ,57007
58
59
  htmlgraph/session_hooks.py,sha256=AOjpPMZo2s_d_KXuPiWDo8TIOdkXsEU0CoOHd4G5A10,9195
59
- htmlgraph/session_manager.py,sha256=X-xm9JvfbMt2V2dI9I_lgiosqTV7YnHTLXlwy7F7FZw,101084
60
+ htmlgraph/session_manager.py,sha256=oRuj_Et9kyOfcn-NKaphdh58xK0i3iR9924CLmv0kzQ,101420
60
61
  htmlgraph/session_registry.py,sha256=xF6VaFp3tE9LA8PXXnaIs2PKZxMOktxRUJJ7vCs0Mcs,18725
61
62
  htmlgraph/session_state.py,sha256=2UcgQNzwBjHkVuGxrAGfHkODopqDexU4wjy9qXKcQIk,15245
62
63
  htmlgraph/session_warning.py,sha256=XPT2Cbg6YRNNhw7NpM8aAp5ny-qfCsYY86DURL_eAPc,7878
@@ -92,10 +93,20 @@ htmlgraph/analytics/strategic/pattern_detector.py,sha256=5SEUpzX5wXsX1tvzkG0oMVG
92
93
  htmlgraph/analytics/strategic/preference_manager.py,sha256=uO8cX-kaK7EIJh-ovXh5cMc0qkMhUfC6zavvvb7osnc,24776
93
94
  htmlgraph/analytics/strategic/suggestion_engine.py,sha256=KV43DF6PETJkmrMA_Ni34heSdQqS_GXKeeGsq5zO6ck,26093
94
95
  htmlgraph/api/__init__.py,sha256=PPFJ5QSv-Zei8U0yGOSs8dJKtUQMeloEqsxVBfzNbbA,105
95
- htmlgraph/api/cost_alerts_websocket.py,sha256=6e_2pKn0-Hcz2ClFyNuJEhVBffqmcUBOkbseSgSkAqQ,14839
96
- htmlgraph/api/main.py,sha256=rQ9758JJ3qqabr8qmQDWuW2T7XlfnDWdRCydPyD9R6o,98121
97
- htmlgraph/api/websocket.py,sha256=L8a_p9cSLdmG1dcVQLZSfxS994Dm-caNPnYQWZ_a-Z0,17592
96
+ htmlgraph/api/broadcast.py,sha256=G2vsgbUFOeahy8l1GJwu5X2yBJAID02lgNXxyDMexx0,9051
97
+ htmlgraph/api/broadcast_routes.py,sha256=OJQaUx6_izB_TlyL5N7tzTx3nD9sFs3RUhehlRtl_3k,11376
98
+ htmlgraph/api/broadcast_websocket.py,sha256=ZTHmLsYCExaCkHiDD7LNKQaAjaTQadNoO4OZOrGsU3Q,3822
99
+ htmlgraph/api/cost_alerts_websocket.py,sha256=jmPJ0kO7PKXYe-MwN0rReQ6ACa6E--p9DaMCqDDs3IE,14478
100
+ htmlgraph/api/main.py,sha256=5VNbi2_l7q9etjr4YRe97ZyeLuNfCqsrvzCQ1-d-azY,102943
101
+ htmlgraph/api/offline.py,sha256=u6RbmeoDPvPsFJYsTNhFZ6yoNE0bgYXFzUi2g2pmeYU,25881
102
+ htmlgraph/api/presence.py,sha256=eF7j5yp7lgO9tF0_VMdE0zeWrUYIAYhgOUHSKe8dr70,14246
103
+ htmlgraph/api/reactive.py,sha256=WW8bBCiu9RBCgNeKE-fohvklJNBnvWj_zdllPkZun2M,14941
104
+ htmlgraph/api/reactive_routes.py,sha256=YdXSpkh8GmmvUhmgXdnJmEJY6Lu0jLTm6QM6BVng2-4,6032
105
+ htmlgraph/api/sync_routes.py,sha256=0ei-TYi0axEAK1aZYbobBIfRsvZ7csiQjJ-zbi-0oQg,4863
106
+ htmlgraph/api/websocket.py,sha256=bO0WS985Ej_27rwoFKy3fFcNLYjV0obvAIju8WzKURY,19915
107
+ htmlgraph/api/static/broadcast-demo.html,sha256=kJ45f9AmHmtCZnf2O3hsfGxa2RAgXLFqxbA689jY8Cg,12611
98
108
  htmlgraph/api/static/htmx.min.js,sha256=0VEHzH8ECp6DsbZhdv2SetQLXgJVgToD-Mz-7UbuQrA,48036
109
+ htmlgraph/api/static/presence-widget-demo.html,sha256=DsjnF-nPiQr6YOWwJ9LvBlN4ZPYTNAbpEGmyI9II03I,26229
99
110
  htmlgraph/api/static/style-redesign.css,sha256=zp0ggDJHz81FN5SW5tRcWXPK_jVNCAJyqKhwlZkBfnQ,26759
100
111
  htmlgraph/api/static/style.css,sha256=6nUzPvf3Xri0vxbEIunrbJkujP6k3HrM3KC5nrkOjyw,20022
101
112
  htmlgraph/api/templates/dashboard-redesign.html,sha256=n8BjsXJ_Ze7UULGgj6T1QruHXCI1rLHeOiOsqAiBpr4,51978
@@ -103,7 +114,7 @@ htmlgraph/api/templates/dashboard.html,sha256=FEWcnTsxrpblrc6Xcg6ungKWV4Fmm4AQMN
103
114
  htmlgraph/api/templates/partials/activity-feed-hierarchical.html,sha256=28N1IeL84Fh4uYv6DxwivnzCm08LoXf6OWR2GDVvAnY,14175
104
115
  htmlgraph/api/templates/partials/activity-feed.html,sha256=u88O86IjGjCy9hfhfAtmtuaj3FEtiNqWBzh_qMBFGGk,33636
105
116
  htmlgraph/api/templates/partials/agents-redesign.html,sha256=32sZ596ShucaLW7kUh95mB_znJeK7p8eyJ5F-wxkXzw,10266
106
- htmlgraph/api/templates/partials/agents.html,sha256=32sZ596ShucaLW7kUh95mB_znJeK7p8eyJ5F-wxkXzw,10266
117
+ htmlgraph/api/templates/partials/agents.html,sha256=R6d0duU1biovC90gnTnNMjKuOMdZcz6kAmFOOaX-8tk,18417
107
118
  htmlgraph/api/templates/partials/event-traces.html,sha256=asZ_TjZT_xqbAqN2NU3hbdxExsQFL7t9LLaXw5NrOfU,10417
108
119
  htmlgraph/api/templates/partials/features-kanban-redesign.html,sha256=36lEnE-zZK0s2YZrOHCzroorEvGnCzzoem39Z5Q0SgI,18949
109
120
  htmlgraph/api/templates/partials/features.html,sha256=M-fELKjyh01_qyFnmh5N6pKXjkfp75GS9WNSCd5cy0Q,22192
@@ -164,6 +175,7 @@ htmlgraph/cli/work/snapshot.py,sha256=ZJFF8ea8WlD1EP2iFXfhKK2mfUufWbe_cYvMPcmATo
164
175
  htmlgraph/cli/work/tracks.py,sha256=MzymITEkbq_jNeCYGLgqWwwa0hAv3UWpPqhEMTHC8zI,16290
165
176
  htmlgraph/cli_commands/__init__.py,sha256=GYdoNv80KS2p7bCThuoRbhiLmIZW6ddPqPEFEx567nQ,35
166
177
  htmlgraph/cli_commands/feature.py,sha256=KqC0daKE1aMb9-FBHlEEUZbLFvZdjlW5CxqSeO1HVVU,6689
178
+ htmlgraph/cli_commands/sync.py,sha256=3XmqCPgZ11cZ3aAs4523oS54ORlgU24IjioE23XslkQ,5795
167
179
  htmlgraph/collections/__init__.py,sha256=bDUTK5grqWKCR5sawI8fa-XoL09p5-heReGdkdSXArY,1163
168
180
  htmlgraph/collections/base.py,sha256=Wbpf_5Z340bds4mohzjS_w9fB-qKG01hBa81NkWnDtE,26728
169
181
  htmlgraph/collections/bug.py,sha256=uqOWAtJShwZtDYEfsrs6kMnTDUrqhykezMd72_WMaXE,1332
@@ -184,7 +196,7 @@ htmlgraph/cost_analysis/__init__.py,sha256=kuAA4Fd0gUplOWOiqs3CF4eO1wwekGs_3DaiF
184
196
  htmlgraph/cost_analysis/analyzer.py,sha256=TDKgRXeOQ-td5LZhkFob4ajEf-JssSCGKlMS3GysYJU,15040
185
197
  htmlgraph/db/__init__.py,sha256=1yWTyWrN2kcmW6mVKCUf4POSUXTOSaAljwEyyC0Xs1w,1033
186
198
  htmlgraph/db/queries.py,sha256=FtqOIBJC0EociEYNJr6gTVROqd9m5yZdVGYLxXNVSdk,22608
187
- htmlgraph/db/schema.py,sha256=aVo9jeYW_rgPv0JB5RpXm7ZdzOyljLfc3sJwuQRtrtU,69262
199
+ htmlgraph/db/schema.py,sha256=ZjKwEaKtNpUgSBqEmKLhv1tGwv1_XaJ0NvsZOeXP1cc,78914
188
200
  htmlgraph/docs/API_REFERENCE.md,sha256=LYsYeHinF6GEsjMvrgdQSbgmMPsOh4ZQ1WK11niqNvo,17862
189
201
  htmlgraph/docs/HTTP_API.md,sha256=IrX-wZckFn-K3ztCVcT-zyGqJL2TtY1qYV7dUuC7kzc,13344
190
202
  htmlgraph/docs/INTEGRATION_GUIDE.md,sha256=uNNM0ipY0bFAkZaL1CkvnA_Wt2MVPGRBdA4927cZaHo,16910
@@ -214,7 +226,7 @@ htmlgraph/hooks/cigs_pretool_enforcer.py,sha256=LC5y5IrKdTx2aq5ht5U7Tceq0k63-7EI
214
226
  htmlgraph/hooks/concurrent_sessions.py,sha256=qOiwDfynphVG0-2pVBakEzOwMORU8ebN1gMjcN4S0z0,6476
215
227
  htmlgraph/hooks/context.py,sha256=tJ4dIL8uTFHyqyuuMc-ETDuOikeD5cN3Mdjmfg6W0HE,13108
216
228
  htmlgraph/hooks/drift_handler.py,sha256=UchkeVmjgI6J4NE4vKNTHsY6ZorvUHvp1FOfwTEY-Cs,17626
217
- htmlgraph/hooks/event_tracker.py,sha256=Cx51c2VExLKWRlJCYYlnOItU9oGgj8gbceOHys00Tjs,51398
229
+ htmlgraph/hooks/event_tracker.py,sha256=yFDGVsRTvVkmEhVPkZGwEz66ymIWZh9hDbuYMEvUkLM,53442
218
230
  htmlgraph/hooks/git_commands.py,sha256=NPzthfzGJ_bkDi7soehHOxI9FLL-6BL8Tie9Byb_zf4,4803
219
231
  htmlgraph/hooks/hooks-config.example.json,sha256=tXpk-U-FZzGOoNJK2uiDMbIHCYEHA794J-El0fBwkqg,197
220
232
  htmlgraph/hooks/installer.py,sha256=NdZHOER7DHFyZYXiZIS-CgLt1ZUxJ4O4ju8HcPjJajs,11816
@@ -323,16 +335,17 @@ htmlgraph/services/__init__.py,sha256=Cy-4RI7raV4jd2Vfc5Zya0VTgV0x9LNt32h9DhYVGk
323
335
  htmlgraph/services/claiming.py,sha256=HcrltEJKN72mxuD7fGuXWeh1U0vwhjMvhZcFc02EiyU,6071
324
336
  htmlgraph/sessions/__init__.py,sha256=58XJkCNtksEalKFhQsovzyewQlI6hrPKXzQOuEuMDy4,429
325
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
326
340
  htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4bet1H4ZsDpI,7642
327
341
  htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
328
342
  htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
329
343
  htmlgraph/templates/orchestration-view.html,sha256=DlS7LlcjH0oO_KYILjuF1X42t8QhKLH4F85rkO54alY,10472
330
- htmlgraph-0.27.7.data/data/htmlgraph/dashboard.html,sha256=MUT6SaYnazoyDcvHz5hN1omYswyIoUfeoZLf2M_iblo,251268
331
- htmlgraph-0.27.7.data/data/htmlgraph/styles.css,sha256=oDUSC8jG-V-hKojOBO9J88hxAeY2wJrBYTq0uCwX_Y4,7135
332
- htmlgraph-0.27.7.data/data/htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4bet1H4ZsDpI,7642
333
- htmlgraph-0.27.7.data/data/htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
334
- htmlgraph-0.27.7.data/data/htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
335
- htmlgraph-0.27.7.dist-info/METADATA,sha256=KZ0Sgub8ZW_BeX0WxY3l7vFzf3L_kOEjfSMtfybjGtM,10220
336
- htmlgraph-0.27.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
337
- htmlgraph-0.27.7.dist-info/entry_points.txt,sha256=Wmdo5cx8pt6NoMsssVE2mZH1CZLSUsrg_3iSWatiyn0,103
338
- htmlgraph-0.27.7.dist-info/RECORD,,
344
+ htmlgraph-0.28.1.data/data/htmlgraph/styles.css,sha256=oDUSC8jG-V-hKojOBO9J88hxAeY2wJrBYTq0uCwX_Y4,7135
345
+ htmlgraph-0.28.1.data/data/htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4bet1H4ZsDpI,7642
346
+ htmlgraph-0.28.1.data/data/htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
347
+ htmlgraph-0.28.1.data/data/htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
348
+ htmlgraph-0.28.1.dist-info/METADATA,sha256=KLN9EhFVOml_AvYRPvK6mOc2n98C3wPIlKaJYSn2Q_Y,10220
349
+ htmlgraph-0.28.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
350
+ htmlgraph-0.28.1.dist-info/entry_points.txt,sha256=Wmdo5cx8pt6NoMsssVE2mZH1CZLSUsrg_3iSWatiyn0,103
351
+ htmlgraph-0.28.1.dist-info/RECORD,,