kore-memory 2.4.0__tar.gz → 2.5.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 (124) hide show
  1. {kore_memory-2.4.0 → kore_memory-2.5.0}/CHANGELOG.md +37 -0
  2. {kore_memory-2.4.0 → kore_memory-2.5.0}/PKG-INFO +3 -1
  3. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/config.py +1 -1
  4. kore_memory-2.5.0/kore_memory/filesystem_watcher.py +343 -0
  5. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/main.py +66 -0
  6. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/models.py +37 -0
  7. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/repository/search.py +3 -1
  8. {kore_memory-2.4.0 → kore_memory-2.5.0}/pyproject.toml +4 -1
  9. {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/package.json +1 -1
  10. kore_memory-2.5.0/tests/test_wave3_watcher.py +415 -0
  11. {kore_memory-2.4.0 → kore_memory-2.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  12. {kore_memory-2.4.0 → kore_memory-2.5.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  13. {kore_memory-2.4.0 → kore_memory-2.5.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  14. {kore_memory-2.4.0 → kore_memory-2.5.0}/.github/workflows/benchmark.yml +0 -0
  15. {kore_memory-2.4.0 → kore_memory-2.5.0}/.github/workflows/build-sdk.yml +0 -0
  16. {kore_memory-2.4.0 → kore_memory-2.5.0}/.github/workflows/ci.yml +0 -0
  17. {kore_memory-2.4.0 → kore_memory-2.5.0}/.github/workflows/publish.yml +0 -0
  18. {kore_memory-2.4.0 → kore_memory-2.5.0}/.gitignore +0 -0
  19. {kore_memory-2.4.0 → kore_memory-2.5.0}/CONTRIBUTING.md +0 -0
  20. {kore_memory-2.4.0 → kore_memory-2.5.0}/LICENSE +0 -0
  21. {kore_memory-2.4.0 → kore_memory-2.5.0}/MIGRATION-v0.6.md +0 -0
  22. {kore_memory-2.4.0 → kore_memory-2.5.0}/README.md +0 -0
  23. {kore_memory-2.4.0 → kore_memory-2.5.0}/article-devto.md +0 -0
  24. {kore_memory-2.4.0 → kore_memory-2.5.0}/assets/favicon.svg +0 -0
  25. {kore_memory-2.4.0 → kore_memory-2.5.0}/assets/logo.svg +0 -0
  26. {kore_memory-2.4.0 → kore_memory-2.5.0}/docs/competitive-analysis-2025.md +0 -0
  27. {kore_memory-2.4.0 → kore_memory-2.5.0}/docs/quickstart-v2.1.md +0 -0
  28. {kore_memory-2.4.0 → kore_memory-2.5.0}/docs/v1.0-roadmap.md +0 -0
  29. {kore_memory-2.4.0 → kore_memory-2.5.0}/examples/async_usage.py +0 -0
  30. {kore_memory-2.4.0 → kore_memory-2.5.0}/examples/basic_usage.py +0 -0
  31. {kore_memory-2.4.0 → kore_memory-2.5.0}/examples/langchain_example.py +0 -0
  32. {kore_memory-2.4.0 → kore_memory-2.5.0}/hatch_build.py +0 -0
  33. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore-daemon.sh +0 -0
  34. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/__init__.py +0 -0
  35. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/acl.py +0 -0
  36. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/analytics.py +0 -0
  37. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/audit.py +0 -0
  38. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/auth.py +0 -0
  39. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/auto_tuner.py +0 -0
  40. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/cli.py +0 -0
  41. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/client.py +0 -0
  42. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/compressor.py +0 -0
  43. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/conflict_detector.py +0 -0
  44. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/context_assembler.py +0 -0
  45. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/dashboard.py +0 -0
  46. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/database.py +0 -0
  47. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/decay.py +0 -0
  48. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/embedder.py +0 -0
  49. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/events.py +0 -0
  50. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/filesystem_overlay.py +0 -0
  51. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/integrations/__init__.py +0 -0
  52. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/integrations/crewai.py +0 -0
  53. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/integrations/entities.py +0 -0
  54. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/integrations/langchain.py +0 -0
  55. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/integrations/openai_agents.py +0 -0
  56. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/integrations/pydantic_ai.py +0 -0
  57. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/mcp_server.py +0 -0
  58. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/plugins.py +0 -0
  59. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/py.typed +0 -0
  60. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/ranking.py +0 -0
  61. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/repository/__init__.py +0 -0
  62. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/repository/graph.py +0 -0
  63. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/repository/lifecycle.py +0 -0
  64. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/repository/memory.py +0 -0
  65. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/repository/sessions.py +0 -0
  66. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/scorer.py +0 -0
  67. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/summarizer.py +0 -0
  68. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/templates/dashboard.html +0 -0
  69. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/vector_index.py +0 -0
  70. {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/welcome.py +0 -0
  71. {kore_memory-2.4.0 → kore_memory-2.5.0}/presets/claude-code/README.md +0 -0
  72. {kore_memory-2.4.0 → kore_memory-2.5.0}/presets/claude-code/mcp.json +0 -0
  73. {kore_memory-2.4.0 → kore_memory-2.5.0}/presets/cursor/README.md +0 -0
  74. {kore_memory-2.4.0 → kore_memory-2.5.0}/presets/cursor/mcp.json +0 -0
  75. {kore_memory-2.4.0 → kore_memory-2.5.0}/requirements.txt +0 -0
  76. {kore_memory-2.4.0 → kore_memory-2.5.0}/scripts/assert_benchmarks.py +0 -0
  77. {kore_memory-2.4.0 → kore_memory-2.5.0}/scripts/import_memory.py +0 -0
  78. {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/.gitignore +0 -0
  79. {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/README.md +0 -0
  80. {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/package-lock.json +0 -0
  81. {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/src/client.ts +0 -0
  82. {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/src/errors.ts +0 -0
  83. {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/src/index.ts +0 -0
  84. {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/src/types.ts +0 -0
  85. {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/tests/client.test.ts +0 -0
  86. {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/tests/errors.test.ts +0 -0
  87. {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/tests/helpers.ts +0 -0
  88. {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/tsconfig.json +0 -0
  89. {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/tsup.config.ts +0 -0
  90. {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/vitest.config.ts +0 -0
  91. {kore_memory-2.4.0 → kore_memory-2.5.0}/start.sh +0 -0
  92. {kore_memory-2.4.0 → kore_memory-2.5.0}/test_pagination_fix.py +0 -0
  93. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/__init__.py +0 -0
  94. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/__init__.py +0 -0
  95. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/conftest_bench.py +0 -0
  96. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/datasets/dataset_a_temporal.json +0 -0
  97. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/datasets/dataset_b_conflicts.json +0 -0
  98. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/datasets/dataset_c_coding.json +0 -0
  99. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/datasets/dataset_d_graph.json +0 -0
  100. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/datasets/dataset_e_context.json +0 -0
  101. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/datasets/loaders.py +0 -0
  102. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/test_benchmarks.py +0 -0
  103. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/conftest.py +0 -0
  104. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_api.py +0 -0
  105. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_audit.py +0 -0
  106. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_auth_events.py +0 -0
  107. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_auto_tuner.py +0 -0
  108. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_cli.py +0 -0
  109. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_client.py +0 -0
  110. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_client_sync.py +0 -0
  111. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_conflict_detection.py +0 -0
  112. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_crewai.py +0 -0
  113. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_dashboard.py +0 -0
  114. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_entities.py +0 -0
  115. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_langchain.py +0 -0
  116. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_mcp.py +0 -0
  117. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_ranking_engine.py +0 -0
  118. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_sessions.py +0 -0
  119. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_v11_fixes.py +0 -0
  120. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_v12_features.py +0 -0
  121. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_v21_temporal.py +0 -0
  122. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_v2_features.py +0 -0
  123. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_wave3_graph.py +0 -0
  124. {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_wave3_overlay.py +0 -0
@@ -11,6 +11,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ---
13
13
 
14
+ ## [2.5.0] - 2026-04-14
15
+
16
+ ### Theme: "Filesystem Watcher — Live Overlay Sync"
17
+
18
+ ### Added
19
+
20
+ #### Filesystem Watcher (`kore_memory/filesystem_watcher.py`)
21
+ - **Nuovo modulo** che monitora in background le directory tramite `watchdog` e aggiorna automaticamente il filesystem overlay quando i file cambiano su disco
22
+ - `_WatcherRegistry`: thread-safe registry dei watcher attivi, keyed by `agent_id:base_path`
23
+ - `_KoreFileHandler`: gestisce CREATE/MODIFY con debounce 1s (evita burst da IDE auto-save), DELETE immediato, MOVE = remove vecchio + reindex nuovo
24
+ - Estensioni monitorate: `.md .rst .toml .txt .json .yaml .yml .py .cfg` + nomi file da `DEFAULT_PATTERNS`
25
+ - **Graceful degradation**: se `watchdog` non è installato, il modulo carica senza crash (`_HAS_WATCHDOG` flag), `start_watcher()` solleva `ImportError` chiaro
26
+ - API pubblica: `start_watcher`, `stop_watcher`, `stop_all_watchers`, `list_watchers`, `is_available`
27
+ - Graceful shutdown: `stop_all_watchers()` chiamato nel lifespan FastAPI prima del pool cleanup
28
+
29
+ #### Nuovi endpoint REST
30
+ - `POST /overlay/watch` — avvia watcher per `base_path` (503 se watchdog non installato, 400 se path non valido)
31
+ - `DELETE /overlay/watch?path=...` — ferma watcher per path/agent
32
+ - `GET /overlay/watchers` — lista watcher attivi con statistiche (`events_processed`, `active`, `started_at`)
33
+
34
+ #### Nuovi modelli Pydantic
35
+ - `OverlayWatchRequest`, `OverlayWatchResponse`, `OverlayWatcherRecord`, `OverlayWatchersResponse`
36
+
37
+ #### Nuova optional dependency
38
+ - `pip install 'kore-memory[watcher]'` installa `watchdog>=4.0.0`
39
+
40
+ ### Tests
41
+ - **35 test** nel nuovo `tests/test_wave3_watcher.py` (2 skip se watchdog non installato):
42
+ - `TestWatcherAvailability` (3 test): flag, is_available(), ImportError senza watchdog
43
+ - `TestWatcherRegistry` (8 test): key format, add/get/remove, stop_all
44
+ - `TestKoreFileHandler` (12 test): is_relevant, debounce, on_modified/created/deleted/moved, do_reindex/do_remove
45
+ - `TestStartStopWatcher` (7 test): start/stop lifecycle, already_running, list
46
+ - `TestWatcherEndpoints` (6 test): REST API GET/POST/DELETE + full lifecycle
47
+ - Suite totale: **629 test** (da 572 → +57 inclusi test v2.4.0 precedenti)
48
+
49
+ ---
50
+
14
51
  ## [2.4.0] - 2026-04-14
15
52
 
16
53
  ### Theme: "Dashboard Fixes + MCP Auto-Session"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kore-memory
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: The memory layer that thinks like a human: remembers what matters, forgets what doesn't, and never calls home.
5
5
  Project-URL: Homepage, https://github.com/auriti-labs/kore-memory
6
6
  Project-URL: Documentation, https://auritidesign.it/docs
@@ -43,6 +43,8 @@ Requires-Dist: pydantic-ai>=0.0.49; extra == 'pydantic-ai'
43
43
  Provides-Extra: semantic
44
44
  Requires-Dist: sentence-transformers>=3.0.0; extra == 'semantic'
45
45
  Requires-Dist: sqlite-vec>=0.1.1; extra == 'semantic'
46
+ Provides-Extra: watcher
47
+ Requires-Dist: watchdog>=4.0.0; extra == 'watcher'
46
48
  Description-Content-Type: text/markdown
47
49
 
48
50
  <div align="center">
@@ -85,4 +85,4 @@ CONFLICT_MAX_CANDIDATES = int(os.getenv("KORE_CONFLICT_MAX_CANDIDATES", "10"))
85
85
 
86
86
  # ── Version ───────────────────────────────────────────────────────────────────
87
87
 
88
- VERSION = "2.4.0"
88
+ VERSION = "2.5.0"
@@ -0,0 +1,343 @@
1
+ """
2
+ Kore — Filesystem Watcher (Wave 3, issue #025)
3
+
4
+ Auto-aggiorna il filesystem overlay quando i file monitorati cambiano su disco.
5
+ Richiede: pip install kore-memory[watcher] (watchdog>=4.0.0)
6
+
7
+ Funzionalità:
8
+ - Monitora i file dell'overlay in background via watchdog Observer
9
+ - CREATE/MODIFY → re-index con debounce da 1s (evita burst da auto-save IDE)
10
+ - DELETE → remove_file_from_overlay()
11
+ - Watcher per (base_path, agent_id) — multipli watchers supportati
12
+ - Graceful degradation: se watchdog non disponibile, errori chiari senza crash
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import threading
19
+ from datetime import UTC, datetime
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ from .filesystem_overlay import DEFAULT_PATTERNS, index_files, remove_file_from_overlay
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # ── Disponibilità watchdog ────────────────────────────────────────────────────
28
+
29
+ try:
30
+ from watchdog.events import FileSystemEventHandler
31
+ from watchdog.observers import Observer as _Observer
32
+
33
+ _HAS_WATCHDOG = True
34
+ except ImportError:
35
+ _HAS_WATCHDOG = False
36
+ # Fallback per type annotations senza watchdog installato
37
+ FileSystemEventHandler = object # type: ignore[assignment,misc]
38
+
39
+ _DEBOUNCE_SECONDS = 1.0 # attesa dopo l'ultimo evento prima di re-indexare
40
+
41
+ # Estensioni di file monitorate dal watcher
42
+ _WATCHED_EXTENSIONS = frozenset({".md", ".rst", ".toml", ".txt", ".json", ".yaml", ".yml", ".py", ".cfg"})
43
+
44
+
45
+ # ── Struttura dati watcher ────────────────────────────────────────────────────
46
+
47
+
48
+ class _WatcherEntry:
49
+ """Rappresenta un watcher attivo per una (base_path, agent_id)."""
50
+
51
+ def __init__(
52
+ self,
53
+ base_path: str,
54
+ agent_id: str,
55
+ patterns: list[str] | None,
56
+ include_extra_md: bool,
57
+ max_depth: int,
58
+ ) -> None:
59
+ self.base_path = base_path
60
+ self.agent_id = agent_id
61
+ self.patterns = patterns
62
+ self.include_extra_md = include_extra_md
63
+ self.max_depth = max_depth
64
+ self.observer: Any = None
65
+ self.started_at: str = datetime.now(UTC).isoformat()
66
+ self.events_processed: int = 0
67
+
68
+
69
+ class _WatcherRegistry:
70
+ """Registry thread-safe dei watcher attivi."""
71
+
72
+ def __init__(self) -> None:
73
+ self._watchers: dict[str, _WatcherEntry] = {}
74
+ self._lock = threading.Lock()
75
+
76
+ @staticmethod
77
+ def _key(base_path: str, agent_id: str) -> str:
78
+ return f"{agent_id}:{base_path}"
79
+
80
+ def add(self, entry: _WatcherEntry) -> None:
81
+ with self._lock:
82
+ self._watchers[self._key(entry.base_path, entry.agent_id)] = entry
83
+
84
+ def remove(self, base_path: str, agent_id: str) -> _WatcherEntry | None:
85
+ with self._lock:
86
+ return self._watchers.pop(self._key(base_path, agent_id), None)
87
+
88
+ def get(self, base_path: str, agent_id: str) -> _WatcherEntry | None:
89
+ with self._lock:
90
+ return self._watchers.get(self._key(base_path, agent_id))
91
+
92
+ def list_all(self) -> list[dict]:
93
+ with self._lock:
94
+ return [
95
+ {
96
+ "base_path": e.base_path,
97
+ "agent_id": e.agent_id,
98
+ "started_at": e.started_at,
99
+ "events_processed": e.events_processed,
100
+ "active": e.observer is not None and e.observer.is_alive(),
101
+ }
102
+ for e in self._watchers.values()
103
+ ]
104
+
105
+ def stop_all(self) -> int:
106
+ """Ferma tutti i watcher — chiamato al shutdown del server."""
107
+ with self._lock:
108
+ entries = list(self._watchers.values())
109
+ self._watchers.clear()
110
+ count = 0
111
+ for entry in entries:
112
+ if entry.observer:
113
+ try:
114
+ entry.observer.stop()
115
+ entry.observer.join(timeout=2)
116
+ count += 1
117
+ except Exception:
118
+ pass
119
+ return count
120
+
121
+
122
+ _registry = _WatcherRegistry()
123
+
124
+
125
+ # ── Event handler ─────────────────────────────────────────────────────────────
126
+
127
+
128
+ class _KoreFileHandler(FileSystemEventHandler): # type: ignore[misc]
129
+ """Gestisce gli eventi del filesystem e aggiorna l'overlay con debounce."""
130
+
131
+ def __init__(self, entry: _WatcherEntry) -> None:
132
+ if _HAS_WATCHDOG:
133
+ super().__init__()
134
+ self._entry = entry
135
+ self._timers: dict[str, threading.Timer] = {}
136
+ self._timers_lock = threading.Lock()
137
+
138
+ # ── Debounce ──────────────────────────────────────────────────────────────
139
+
140
+ def _schedule_reindex(self, filepath: str) -> None:
141
+ """Annulla il timer pendente e ne avvia uno nuovo (debounce 1s)."""
142
+ with self._timers_lock:
143
+ existing = self._timers.get(filepath)
144
+ if existing:
145
+ existing.cancel()
146
+ timer = threading.Timer(_DEBOUNCE_SECONDS, self._do_reindex, args=[filepath])
147
+ self._timers[filepath] = timer
148
+ timer.start()
149
+
150
+ def _do_reindex(self, filepath: str) -> None:
151
+ """Re-indicizza il file nel filesystem overlay."""
152
+ with self._timers_lock:
153
+ self._timers.pop(filepath, None)
154
+
155
+ try:
156
+ path = Path(filepath)
157
+ if not path.exists() or not path.is_file():
158
+ return
159
+ stats = index_files(
160
+ filepaths=[path],
161
+ agent_id=self._entry.agent_id,
162
+ replace_existing=True,
163
+ )
164
+ self._entry.events_processed += 1
165
+ logger.info(
166
+ "Watcher re-indexed %s (agent=%s, updated=%d, indexed=%d)",
167
+ filepath,
168
+ self._entry.agent_id,
169
+ stats.get("updated", 0),
170
+ stats.get("indexed", 0),
171
+ )
172
+ except Exception as exc:
173
+ logger.error("Watcher: errore re-index %s: %s", filepath, exc)
174
+
175
+ def _do_remove(self, filepath: str) -> None:
176
+ """Rimuove le memories del file eliminato dall'overlay."""
177
+ try:
178
+ removed = remove_file_from_overlay(filepath, agent_id=self._entry.agent_id)
179
+ if removed:
180
+ self._entry.events_processed += 1
181
+ logger.info(
182
+ "Watcher: rimosso overlay per file eliminato %s (agent=%s, memories=%d)",
183
+ filepath,
184
+ self._entry.agent_id,
185
+ removed,
186
+ )
187
+ except Exception as exc:
188
+ logger.error("Watcher: errore rimozione overlay %s: %s", filepath, exc)
189
+
190
+ # ── Filtro file ───────────────────────────────────────────────────────────
191
+
192
+ def _is_relevant(self, filepath: str) -> bool:
193
+ """
194
+ True se il file è dentro base_path e ha un'estensione o nome rilevante.
195
+ """
196
+ path = Path(filepath)
197
+ try:
198
+ path.relative_to(self._entry.base_path)
199
+ except ValueError:
200
+ return False
201
+
202
+ if path.suffix.lower() in _WATCHED_EXTENSIONS:
203
+ return True
204
+
205
+ watched_names = set(self._entry.patterns or DEFAULT_PATTERNS)
206
+ return path.name in watched_names
207
+
208
+ # ── Handlers watchdog ─────────────────────────────────────────────────────
209
+
210
+ def on_modified(self, event: Any) -> None:
211
+ if not event.is_directory and self._is_relevant(event.src_path):
212
+ self._schedule_reindex(event.src_path)
213
+
214
+ def on_created(self, event: Any) -> None:
215
+ if not event.is_directory and self._is_relevant(event.src_path):
216
+ self._schedule_reindex(event.src_path)
217
+
218
+ def on_deleted(self, event: Any) -> None:
219
+ if not event.is_directory:
220
+ self._do_remove(event.src_path)
221
+
222
+ def on_moved(self, event: Any) -> None:
223
+ """Rinomina = elimina vecchio + indicizza nuovo."""
224
+ if not event.is_directory:
225
+ self._do_remove(event.src_path)
226
+ if self._is_relevant(event.dest_path):
227
+ self._schedule_reindex(event.dest_path)
228
+
229
+
230
+ # ── API pubblica ──────────────────────────────────────────────────────────────
231
+
232
+
233
+ def start_watcher(
234
+ base_path: str,
235
+ agent_id: str = "default",
236
+ patterns: list[str] | None = None,
237
+ include_extra_md: bool = True,
238
+ max_depth: int = 2,
239
+ ) -> dict:
240
+ """
241
+ Avvia un watcher per base_path. Richiede watchdog installato.
242
+
243
+ Raises:
244
+ ImportError: se watchdog non è installato
245
+ ValueError: se base_path non è una directory esistente
246
+ """
247
+ if not _HAS_WATCHDOG:
248
+ raise ImportError("watchdog non installato. Eseguire: pip install 'kore-memory[watcher]'")
249
+
250
+ resolved = str(Path(base_path).resolve())
251
+ if not Path(resolved).is_dir():
252
+ raise ValueError(f"base_path deve essere una directory esistente: {resolved!r}")
253
+
254
+ # Watcher già attivo per questa coppia (path, agent)?
255
+ existing = _registry.get(resolved, agent_id)
256
+ if existing and existing.observer and existing.observer.is_alive():
257
+ return {
258
+ "watching": True,
259
+ "already_running": True,
260
+ "base_path": resolved,
261
+ "agent_id": agent_id,
262
+ "started_at": existing.started_at,
263
+ "message": "Watcher già attivo",
264
+ }
265
+
266
+ entry = _WatcherEntry(
267
+ base_path=resolved,
268
+ agent_id=agent_id,
269
+ patterns=patterns,
270
+ include_extra_md=include_extra_md,
271
+ max_depth=max_depth,
272
+ )
273
+
274
+ handler = _KoreFileHandler(entry)
275
+ observer = _Observer()
276
+ observer.schedule(handler, resolved, recursive=True)
277
+ observer.start()
278
+
279
+ entry.observer = observer
280
+ _registry.add(entry)
281
+
282
+ logger.info("Watcher avviato: base_path=%s agent=%s", resolved, agent_id)
283
+ return {
284
+ "watching": True,
285
+ "already_running": False,
286
+ "base_path": resolved,
287
+ "agent_id": agent_id,
288
+ "started_at": entry.started_at,
289
+ "message": "Watcher avviato",
290
+ }
291
+
292
+
293
+ def stop_watcher(base_path: str, agent_id: str = "default") -> dict:
294
+ """Ferma il watcher per (base_path, agent_id)."""
295
+ resolved = str(Path(base_path).resolve())
296
+ entry = _registry.remove(resolved, agent_id)
297
+
298
+ if not entry:
299
+ return {
300
+ "stopped": False,
301
+ "base_path": resolved,
302
+ "agent_id": agent_id,
303
+ "message": "Nessun watcher attivo trovato",
304
+ }
305
+
306
+ if entry.observer:
307
+ try:
308
+ entry.observer.stop()
309
+ entry.observer.join(timeout=3)
310
+ except Exception as exc:
311
+ logger.warning("Errore stop observer %s: %s", resolved, exc)
312
+
313
+ logger.info(
314
+ "Watcher fermato: base_path=%s agent=%s events=%d",
315
+ resolved,
316
+ agent_id,
317
+ entry.events_processed,
318
+ )
319
+ return {
320
+ "stopped": True,
321
+ "base_path": resolved,
322
+ "agent_id": agent_id,
323
+ "events_processed": entry.events_processed,
324
+ "message": "Watcher fermato",
325
+ }
326
+
327
+
328
+ def stop_all_watchers() -> int:
329
+ """Ferma tutti i watcher attivi. Chiamato al shutdown del server."""
330
+ count = _registry.stop_all()
331
+ if count:
332
+ logger.info("Watcher: fermati %d observer al shutdown", count)
333
+ return count
334
+
335
+
336
+ def list_watchers() -> list[dict]:
337
+ """Restituisce la lista dei watcher attivi con statistiche."""
338
+ return _registry.list_all()
339
+
340
+
341
+ def is_available() -> bool:
342
+ """True se watchdog è installato e il watcher è disponibile."""
343
+ return _HAS_WATCHDOG
@@ -57,6 +57,10 @@ from .models import (
57
57
  OverlayFilesResponse,
58
58
  OverlayIndexRequest,
59
59
  OverlayIndexResponse,
60
+ OverlayWatcherRecord,
61
+ OverlayWatchersResponse,
62
+ OverlayWatchRequest,
63
+ OverlayWatchResponse,
60
64
  PluginListResponse,
61
65
  RelationRecord,
62
66
  RelationRequest,
@@ -222,6 +226,10 @@ async def lifespan(app: FastAPI):
222
226
 
223
227
  register_audit_handler()
224
228
  yield
229
+ # Graceful shutdown: ferma i watcher attivi
230
+ from .filesystem_watcher import stop_all_watchers
231
+
232
+ stop_all_watchers()
225
233
  # Graceful shutdown: close the SQLite connection pool
226
234
  from .database import _pool
227
235
 
@@ -1283,6 +1291,64 @@ def overlay_remove_file(
1283
1291
  return {"removed": removed, "path": path}
1284
1292
 
1285
1293
 
1294
+ # ── Filesystem Watcher (issue #025) ──────────────────────────────────────────
1295
+
1296
+
1297
+ @app.post("/overlay/watch", response_model=OverlayWatchResponse)
1298
+ def overlay_watch_start(
1299
+ body: OverlayWatchRequest,
1300
+ _: str = _Auth,
1301
+ agent_id: str = _Agent,
1302
+ ) -> OverlayWatchResponse:
1303
+ """
1304
+ Avvia un watcher su base_path che auto-aggiorna l'overlay al cambio dei file.
1305
+ Richiede: pip install kore-memory[watcher]
1306
+ """
1307
+ from .filesystem_watcher import start_watcher
1308
+
1309
+ try:
1310
+ patterns = body.patterns if body.patterns else None
1311
+ result = start_watcher(
1312
+ base_path=body.base_path,
1313
+ agent_id=agent_id,
1314
+ patterns=patterns,
1315
+ include_extra_md=body.include_extra_md,
1316
+ max_depth=body.max_depth,
1317
+ )
1318
+ return OverlayWatchResponse(**result)
1319
+ except ImportError as exc:
1320
+ raise HTTPException(status_code=503, detail=str(exc)) from exc
1321
+ except ValueError as exc:
1322
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
1323
+
1324
+
1325
+ @app.delete("/overlay/watch", response_model=dict)
1326
+ def overlay_watch_stop(
1327
+ path: str = Query(..., description="base_path del watcher da fermare"),
1328
+ _: str = _Auth,
1329
+ agent_id: str = _Agent,
1330
+ ) -> dict:
1331
+ """Ferma il watcher attivo per base_path."""
1332
+ from .filesystem_watcher import stop_watcher
1333
+
1334
+ return stop_watcher(base_path=path, agent_id=agent_id)
1335
+
1336
+
1337
+ @app.get("/overlay/watchers", response_model=OverlayWatchersResponse)
1338
+ def overlay_watchers(
1339
+ _: str = _Auth,
1340
+ ) -> OverlayWatchersResponse:
1341
+ """Lista tutti i watcher attivi con statistiche."""
1342
+ from .filesystem_watcher import is_available, list_watchers
1343
+
1344
+ watchers = list_watchers()
1345
+ return OverlayWatchersResponse(
1346
+ watchers=[OverlayWatcherRecord(**w) for w in watchers],
1347
+ total=len(watchers),
1348
+ watcher_available=is_available(),
1349
+ )
1350
+
1351
+
1286
1352
  # ── Audit log ────────────────────────────────────────────────────────────────
1287
1353
 
1288
1354
 
@@ -443,6 +443,43 @@ class OverlayFilesResponse(BaseModel):
443
443
  total: int
444
444
 
445
445
 
446
+ # ── Filesystem Watcher (issue #025) ──────────────────────────────────────────
447
+
448
+
449
+ class OverlayWatchRequest(BaseModel):
450
+ base_path: str = Field(..., min_length=1, max_length=1024, description="Directory da monitorare")
451
+ patterns: list[str] = Field(
452
+ default_factory=list,
453
+ description="Pattern filename da monitorare (vuoto = DEFAULT_PATTERNS)",
454
+ max_length=50,
455
+ )
456
+ include_extra_md: bool = Field(True, description="Monitora anche file .md extra")
457
+ max_depth: int = Field(2, ge=1, le=5, description="Profondità massima di scansione al re-index")
458
+
459
+
460
+ class OverlayWatcherRecord(BaseModel):
461
+ base_path: str
462
+ agent_id: str
463
+ started_at: str
464
+ events_processed: int
465
+ active: bool
466
+
467
+
468
+ class OverlayWatchResponse(BaseModel):
469
+ watching: bool
470
+ already_running: bool = False
471
+ base_path: str
472
+ agent_id: str
473
+ started_at: str = ""
474
+ message: str
475
+
476
+
477
+ class OverlayWatchersResponse(BaseModel):
478
+ watchers: list[OverlayWatcherRecord]
479
+ total: int
480
+ watcher_available: bool
481
+
482
+
446
483
  # ── Summarization ────────────────────────────────────────────────────────────
447
484
 
448
485
 
@@ -346,7 +346,9 @@ def _count_active_memories(
346
346
  params: dict = {"query": safe_query, "agent_id": agent_id}
347
347
  else:
348
348
  # Caso speciale: q=* → conta tutte le memorie attive (wildcard globale)
349
- escaped = "" if query.strip() == "*" else query.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
349
+ escaped = (
350
+ "" if query.strip() == "*" else query.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
351
+ )
350
352
  sql = f"""
351
353
  SELECT COUNT(*) FROM memories
352
354
  WHERE content LIKE :query ESCAPE '\\'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "kore-memory"
7
- version = "2.4.0"
7
+ version = "2.5.0"
8
8
  description = "The memory layer that thinks like a human: remembers what matters, forgets what doesn't, and never calls home."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -50,6 +50,9 @@ pydantic-ai = [
50
50
  openai-agents = [
51
51
  "openai-agents>=0.2.0",
52
52
  ]
53
+ watcher = [
54
+ "watchdog>=4.0.0",
55
+ ]
53
56
  dev = [
54
57
  "pytest>=8.0.0",
55
58
  "pytest-cov>=5.0.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kore-memory-client",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "JavaScript/TypeScript client for Kore Memory - the memory layer that thinks like a human",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",