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.
- {kore_memory-2.4.0 → kore_memory-2.5.0}/CHANGELOG.md +37 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/PKG-INFO +3 -1
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/config.py +1 -1
- kore_memory-2.5.0/kore_memory/filesystem_watcher.py +343 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/main.py +66 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/models.py +37 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/repository/search.py +3 -1
- {kore_memory-2.4.0 → kore_memory-2.5.0}/pyproject.toml +4 -1
- {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/package.json +1 -1
- kore_memory-2.5.0/tests/test_wave3_watcher.py +415 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/.github/workflows/benchmark.yml +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/.github/workflows/build-sdk.yml +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/.github/workflows/ci.yml +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/.github/workflows/publish.yml +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/.gitignore +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/CONTRIBUTING.md +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/LICENSE +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/MIGRATION-v0.6.md +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/README.md +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/article-devto.md +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/assets/favicon.svg +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/assets/logo.svg +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/docs/competitive-analysis-2025.md +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/docs/quickstart-v2.1.md +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/docs/v1.0-roadmap.md +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/examples/async_usage.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/examples/basic_usage.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/examples/langchain_example.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/hatch_build.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore-daemon.sh +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/__init__.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/acl.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/analytics.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/audit.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/auth.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/auto_tuner.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/cli.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/client.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/compressor.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/conflict_detector.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/context_assembler.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/dashboard.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/database.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/decay.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/embedder.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/events.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/filesystem_overlay.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/integrations/__init__.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/integrations/crewai.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/integrations/entities.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/integrations/langchain.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/integrations/openai_agents.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/integrations/pydantic_ai.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/mcp_server.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/plugins.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/py.typed +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/ranking.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/repository/__init__.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/repository/graph.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/repository/lifecycle.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/repository/memory.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/repository/sessions.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/scorer.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/summarizer.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/templates/dashboard.html +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/vector_index.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/kore_memory/welcome.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/presets/claude-code/README.md +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/presets/claude-code/mcp.json +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/presets/cursor/README.md +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/presets/cursor/mcp.json +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/requirements.txt +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/scripts/assert_benchmarks.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/scripts/import_memory.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/.gitignore +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/README.md +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/package-lock.json +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/src/client.ts +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/src/errors.ts +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/src/index.ts +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/src/types.ts +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/tests/client.test.ts +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/tests/errors.test.ts +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/tests/helpers.ts +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/tsconfig.json +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/tsup.config.ts +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/sdk/js/vitest.config.ts +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/start.sh +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/test_pagination_fix.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/__init__.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/__init__.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/conftest_bench.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/datasets/dataset_a_temporal.json +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/datasets/dataset_b_conflicts.json +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/datasets/dataset_c_coding.json +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/datasets/dataset_d_graph.json +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/datasets/dataset_e_context.json +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/datasets/loaders.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/benchmarks/test_benchmarks.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/conftest.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_api.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_audit.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_auth_events.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_auto_tuner.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_cli.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_client.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_client_sync.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_conflict_detection.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_crewai.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_dashboard.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_entities.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_langchain.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_mcp.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_ranking_engine.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_sessions.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_v11_fixes.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_v12_features.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_v21_temporal.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_v2_features.py +0 -0
- {kore_memory-2.4.0 → kore_memory-2.5.0}/tests/test_wave3_graph.py +0 -0
- {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.
|
|
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">
|
|
@@ -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 =
|
|
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.
|
|
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",
|