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.
- htmlgraph/__init__.py +1 -1
- htmlgraph/api/broadcast.py +316 -0
- htmlgraph/api/broadcast_routes.py +357 -0
- htmlgraph/api/broadcast_websocket.py +115 -0
- htmlgraph/api/cost_alerts_websocket.py +7 -16
- htmlgraph/api/main.py +135 -1
- htmlgraph/api/offline.py +776 -0
- htmlgraph/api/presence.py +446 -0
- htmlgraph/api/reactive.py +455 -0
- htmlgraph/api/reactive_routes.py +195 -0
- htmlgraph/api/static/broadcast-demo.html +393 -0
- htmlgraph/api/static/presence-widget-demo.html +785 -0
- htmlgraph/api/sync_routes.py +184 -0
- htmlgraph/api/templates/partials/agents.html +308 -80
- htmlgraph/api/websocket.py +112 -37
- htmlgraph/broadcast_integration.py +227 -0
- htmlgraph/cli_commands/sync.py +207 -0
- htmlgraph/db/schema.py +226 -0
- htmlgraph/hooks/event_tracker.py +53 -2
- htmlgraph/models.py +1 -0
- htmlgraph/reactive_integration.py +148 -0
- htmlgraph/session_manager.py +7 -0
- htmlgraph/sync/__init__.py +21 -0
- htmlgraph/sync/git_sync.py +458 -0
- {htmlgraph-0.27.7.dist-info → htmlgraph-0.28.1.dist-info}/METADATA +1 -1
- {htmlgraph-0.27.7.dist-info → htmlgraph-0.28.1.dist-info}/RECORD +32 -19
- htmlgraph/dashboard.html +0 -6592
- htmlgraph-0.27.7.data/data/htmlgraph/dashboard.html +0 -6592
- {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.27.7.dist-info → htmlgraph-0.28.1.dist-info}/WHEEL +0 -0
- {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.
|
|
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=
|
|
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=
|
|
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=
|
|
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/
|
|
96
|
-
htmlgraph/api/
|
|
97
|
-
htmlgraph/api/
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
331
|
-
htmlgraph-0.
|
|
332
|
-
htmlgraph-0.
|
|
333
|
-
htmlgraph-0.
|
|
334
|
-
htmlgraph-0.
|
|
335
|
-
htmlgraph-0.
|
|
336
|
-
htmlgraph-0.
|
|
337
|
-
htmlgraph-0.
|
|
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,,
|