elesync 1.2.0__tar.gz → 1.3.0__tar.gz

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 (37) hide show
  1. {elesync-1.2.0 → elesync-1.3.0}/PKG-INFO +31 -2
  2. {elesync-1.2.0 → elesync-1.3.0}/README.md +30 -1
  3. {elesync-1.2.0 → elesync-1.3.0}/elesync/__init__.py +1 -1
  4. {elesync-1.2.0 → elesync-1.3.0}/elesync/cli.py +17 -0
  5. {elesync-1.2.0 → elesync-1.3.0}/elesync/usb.py +18 -11
  6. elesync-1.3.0/elesync/web.py +987 -0
  7. {elesync-1.2.0 → elesync-1.3.0}/elesync.egg-info/PKG-INFO +31 -2
  8. {elesync-1.2.0 → elesync-1.3.0}/elesync.egg-info/SOURCES.txt +3 -1
  9. {elesync-1.2.0 → elesync-1.3.0}/pyproject.toml +1 -1
  10. elesync-1.3.0/tests/test_web.py +319 -0
  11. {elesync-1.2.0 → elesync-1.3.0}/LICENSE +0 -0
  12. {elesync-1.2.0 → elesync-1.3.0}/elesync/conflicts.py +0 -0
  13. {elesync-1.2.0 → elesync-1.3.0}/elesync/crypto.py +0 -0
  14. {elesync-1.2.0 → elesync-1.3.0}/elesync/embeddings.py +0 -0
  15. {elesync-1.2.0 → elesync-1.3.0}/elesync/mcp_server.py +0 -0
  16. {elesync-1.2.0 → elesync-1.3.0}/elesync/models.py +0 -0
  17. {elesync-1.2.0 → elesync-1.3.0}/elesync/normalize.py +0 -0
  18. {elesync-1.2.0 → elesync-1.3.0}/elesync/onboarding.py +0 -0
  19. {elesync-1.2.0 → elesync-1.3.0}/elesync/scopes.py +0 -0
  20. {elesync-1.2.0 → elesync-1.3.0}/elesync/service.py +0 -0
  21. {elesync-1.2.0 → elesync-1.3.0}/elesync/store.py +0 -0
  22. {elesync-1.2.0 → elesync-1.3.0}/elesync/summary.py +0 -0
  23. {elesync-1.2.0 → elesync-1.3.0}/elesync/watch.py +0 -0
  24. {elesync-1.2.0 → elesync-1.3.0}/elesync.egg-info/dependency_links.txt +0 -0
  25. {elesync-1.2.0 → elesync-1.3.0}/elesync.egg-info/entry_points.txt +0 -0
  26. {elesync-1.2.0 → elesync-1.3.0}/elesync.egg-info/requires.txt +0 -0
  27. {elesync-1.2.0 → elesync-1.3.0}/elesync.egg-info/top_level.txt +0 -0
  28. {elesync-1.2.0 → elesync-1.3.0}/setup.cfg +0 -0
  29. {elesync-1.2.0 → elesync-1.3.0}/tests/test_conflicts.py +0 -0
  30. {elesync-1.2.0 → elesync-1.3.0}/tests/test_encryption.py +0 -0
  31. {elesync-1.2.0 → elesync-1.3.0}/tests/test_scopes.py +0 -0
  32. {elesync-1.2.0 → elesync-1.3.0}/tests/test_semantic.py +0 -0
  33. {elesync-1.2.0 → elesync-1.3.0}/tests/test_service.py +0 -0
  34. {elesync-1.2.0 → elesync-1.3.0}/tests/test_summary.py +0 -0
  35. {elesync-1.2.0 → elesync-1.3.0}/tests/test_usb.py +0 -0
  36. {elesync-1.2.0 → elesync-1.3.0}/tests/test_vault.py +0 -0
  37. {elesync-1.2.0 → elesync-1.3.0}/tests/test_watch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elesync
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Local-first, MCP-native unified memory vault — your AI memory as files you own, shared across every model.
5
5
  Author: Alphanymous
6
6
  License: MIT
@@ -56,6 +56,7 @@ Dynamic: license-file
56
56
  [![CI](https://github.com/darknodebros/EleSync/actions/workflows/ci.yml/badge.svg)](https://github.com/darknodebros/EleSync/actions/workflows/ci.yml)
57
57
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
58
58
  [![MCP](https://img.shields.io/badge/MCP-native-8A2BE2.svg)](https://modelcontextprotocol.io)
59
+ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=flat&logo=buy-me-a-coffee&logoColor=black)](https://buymeacoffee.com/Darknodebros)
59
60
 
60
61
  <!-- mcp-name: io.github.darknodebros/elesync -->
61
62
 
@@ -84,7 +85,7 @@ pip install "elesync[watch]"
84
85
  ele watch --autostart # runs in the background from now on
85
86
  ```
86
87
 
87
- **Stable at v1.2** — a 134-test suite, CI across Python 3.10–3.14 (including a dev-extras job
88
+ **Stable at v1.3** — a 179-test suite, CI across Python 3.10–3.14 (including a dev-extras job
88
89
  covering encryption and real-model semantic recall) plus ruff lint/format and mypy type-checking,
89
90
  and a public surface (CLI commands, MCP tools, vault layout, export format) you can build on
90
91
  without breakage inside 1.x. See the [CHANGELOG](CHANGELOG.md) for the full history.
@@ -441,6 +442,34 @@ exposed nothing without it. The same XSalsa20-Poly1305 + argon2id encryption use
441
442
  > automatically between the USB vault and your main vault — use `ele export` + `ele import` to
442
443
  > move memories between them.
443
444
 
445
+ ## Local web UI
446
+
447
+ No terminal needed once EleSync is installed — `ele web` opens a clean browser-based
448
+ vault explorer:
449
+
450
+ ```bash
451
+ ele web # opens http://127.0.0.1:7477 in your browser
452
+ ele web --port 8080 # custom port
453
+ ele web --no-browser # start server without auto-opening browser
454
+ ```
455
+
456
+ **What you get:**
457
+
458
+ | Tab | What it does |
459
+ |---|---|
460
+ | **Dashboard** | Total memories, AI source breakdown, FTS / embedding status |
461
+ | **Browse** | Paginated list with live filter, source & type dropdowns |
462
+ | **Search** | Instant full-text search across all memories |
463
+ | **Add Memory** | Manually add a memory (content, source, type, tags) |
464
+ | **Import** | Drag-and-drop a ChatGPT / Claude / Gemini / Grok export JSON |
465
+
466
+ Click any memory to open a slide-in editor — update content, type, tags, or delete it.
467
+
468
+ **Zero extra dependencies** — pure Python stdlib `http.server`. Works with encrypted
469
+ vaults (prompts for passphrase at startup). Binds to `127.0.0.1` only.
470
+
471
+ ---
472
+
444
473
  ## Sync across devices
445
474
 
446
475
  Your vault is **just a folder** (`notes/*.md` + a small `vault.json`), so sync it however you
@@ -16,6 +16,7 @@
16
16
  [![CI](https://github.com/darknodebros/EleSync/actions/workflows/ci.yml/badge.svg)](https://github.com/darknodebros/EleSync/actions/workflows/ci.yml)
17
17
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
18
18
  [![MCP](https://img.shields.io/badge/MCP-native-8A2BE2.svg)](https://modelcontextprotocol.io)
19
+ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=flat&logo=buy-me-a-coffee&logoColor=black)](https://buymeacoffee.com/Darknodebros)
19
20
 
20
21
  <!-- mcp-name: io.github.darknodebros/elesync -->
21
22
 
@@ -44,7 +45,7 @@ pip install "elesync[watch]"
44
45
  ele watch --autostart # runs in the background from now on
45
46
  ```
46
47
 
47
- **Stable at v1.2** — a 134-test suite, CI across Python 3.10–3.14 (including a dev-extras job
48
+ **Stable at v1.3** — a 179-test suite, CI across Python 3.10–3.14 (including a dev-extras job
48
49
  covering encryption and real-model semantic recall) plus ruff lint/format and mypy type-checking,
49
50
  and a public surface (CLI commands, MCP tools, vault layout, export format) you can build on
50
51
  without breakage inside 1.x. See the [CHANGELOG](CHANGELOG.md) for the full history.
@@ -401,6 +402,34 @@ exposed nothing without it. The same XSalsa20-Poly1305 + argon2id encryption use
401
402
  > automatically between the USB vault and your main vault — use `ele export` + `ele import` to
402
403
  > move memories between them.
403
404
 
405
+ ## Local web UI
406
+
407
+ No terminal needed once EleSync is installed — `ele web` opens a clean browser-based
408
+ vault explorer:
409
+
410
+ ```bash
411
+ ele web # opens http://127.0.0.1:7477 in your browser
412
+ ele web --port 8080 # custom port
413
+ ele web --no-browser # start server without auto-opening browser
414
+ ```
415
+
416
+ **What you get:**
417
+
418
+ | Tab | What it does |
419
+ |---|---|
420
+ | **Dashboard** | Total memories, AI source breakdown, FTS / embedding status |
421
+ | **Browse** | Paginated list with live filter, source & type dropdowns |
422
+ | **Search** | Instant full-text search across all memories |
423
+ | **Add Memory** | Manually add a memory (content, source, type, tags) |
424
+ | **Import** | Drag-and-drop a ChatGPT / Claude / Gemini / Grok export JSON |
425
+
426
+ Click any memory to open a slide-in editor — update content, type, tags, or delete it.
427
+
428
+ **Zero extra dependencies** — pure Python stdlib `http.server`. Works with encrypted
429
+ vaults (prompts for passphrase at startup). Binds to `127.0.0.1` only.
430
+
431
+ ---
432
+
404
433
  ## Sync across devices
405
434
 
406
435
  Your vault is **just a folder** (`notes/*.md` + a small `vault.json`), so sync it however you
@@ -9,4 +9,4 @@ from .service import MemoryService
9
9
  from .store import MemoryStore
10
10
 
11
11
  __all__ = ["MemoryItem", "MEMORY_TYPES", "MemoryStore", "MemoryService", "normalize"]
12
- __version__ = "1.2.0"
12
+ __version__ = "1.3.0"
@@ -392,6 +392,17 @@ def cmd_serve(args) -> int:
392
392
  return 0
393
393
 
394
394
 
395
+ def cmd_web(args) -> int:
396
+ from .web import serve
397
+
398
+ serve(
399
+ vault_dir=getattr(args, "vault", None),
400
+ port=getattr(args, "port", 7477),
401
+ open_browser=not getattr(args, "no_browser", False),
402
+ )
403
+ return 0
404
+
405
+
395
406
  def _vault_path(args) -> str:
396
407
  return args.vault or os.environ.get("ELESYNC_DIR", "~/EleSyncVault")
397
408
 
@@ -770,6 +781,12 @@ def build_parser() -> argparse.ArgumentParser:
770
781
  pusb_eject.add_argument("drive", help="Path to the USB drive")
771
782
  pusb_eject.set_defaults(usb_func=cmd_usb_eject)
772
783
 
784
+ # ── web UI ───────────────────────────────────────────────────────────
785
+ pweb = sub.add_parser("web", help="Open the local web UI in your browser")
786
+ pweb.add_argument("--port", type=int, default=7477, help="Port to listen on (default: 7477)")
787
+ pweb.add_argument("--no-browser", action="store_true", help="Don't auto-open the browser")
788
+ pweb.set_defaults(func=cmd_web)
789
+
773
790
  psum = sub.add_parser(
774
791
  "summary",
775
792
  help="Show a human-readable digest of everything in your vault",
@@ -49,6 +49,22 @@ import subprocess
49
49
  import sys
50
50
  from pathlib import Path
51
51
 
52
+ # Module-level references so tests can patch elesync.usb.crypto / MemoryStore / onboarding
53
+ try:
54
+ from . import crypto # type: ignore[attr-defined]
55
+ except Exception: # pragma: no cover
56
+ crypto = None # type: ignore[assignment]
57
+
58
+ try:
59
+ from .store import MemoryStore # type: ignore[attr-defined]
60
+ except Exception: # pragma: no cover
61
+ MemoryStore = None # type: ignore[assignment]
62
+
63
+ try:
64
+ from . import onboarding # type: ignore[attr-defined]
65
+ except Exception: # pragma: no cover
66
+ onboarding = None # type: ignore[assignment]
67
+
52
68
  # ---------------------------------------------------------------------------
53
69
  # Drive detection
54
70
  # ---------------------------------------------------------------------------
@@ -199,10 +215,7 @@ def install(drive: str | Path, passphrase: str, label: str = "USB") -> dict:
199
215
 
200
216
  Returns a result dict with the paths created.
201
217
  """
202
- from . import crypto
203
- from .store import MemoryStore
204
-
205
- if not crypto.available():
218
+ if crypto is None or not crypto.available():
206
219
  raise RuntimeError(
207
220
  'USB vaults are always encrypted. '
208
221
  'Run: pip install "elesync[encryption]" first.'
@@ -252,8 +265,6 @@ def attach(drive: str | Path, passphrase: str, name: str = "elesync-usb") -> dic
252
265
 
253
266
  Returns the result dict from `onboarding.wire_client_config`.
254
267
  """
255
- from . import crypto, onboarding
256
-
257
268
  drive = Path(drive)
258
269
  vp = vault_path(drive)
259
270
 
@@ -303,8 +314,6 @@ def detach(drive: str | Path, name: str = "elesync-usb") -> dict:
303
314
  Call this before physically unplugging the drive so Claude won't try to
304
315
  reconnect to a missing vault on next launch.
305
316
  """
306
- from . import onboarding
307
-
308
317
  cfg_path = onboarding.default_client_config_path()
309
318
  if not cfg_path.exists():
310
319
  return {"removed": False, "reason": "Claude Desktop config not found"}
@@ -356,7 +365,7 @@ def status(drive: str | Path) -> dict:
356
365
  if not result["vault_present"]:
357
366
  return result
358
367
 
359
- result["encrypted"] = crypto.is_encrypted(vp)
368
+ result["encrypted"] = (crypto is not None and crypto.is_encrypted(vp))
360
369
  result["note_count"] = len(list((vp / "notes").glob("*.md*")))
361
370
  return result
362
371
 
@@ -404,8 +413,6 @@ def eject(drive: str | Path) -> str:
404
413
  def _validate_key(vault_dir: Path, key: bytes) -> None:
405
414
  """Raise ValueError if the key cannot decrypt an existing note.
406
415
  If there are no notes yet (empty vault), the key is accepted as valid."""
407
- from . import crypto
408
-
409
416
  encs = sorted((vault_dir / "notes").glob("*.md.enc"))
410
417
  if not encs:
411
418
  return # empty vault — nothing to validate against, trust it