dev-recall 0.2.0__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.
- dev_recall-0.2.0.dist-info/METADATA +281 -0
- dev_recall-0.2.0.dist-info/RECORD +34 -0
- dev_recall-0.2.0.dist-info/WHEEL +5 -0
- dev_recall-0.2.0.dist-info/entry_points.txt +2 -0
- dev_recall-0.2.0.dist-info/top_level.txt +1 -0
- recall/__init__.py +3 -0
- recall/_hooks.py +211 -0
- recall/cli.py +1032 -0
- recall/collectors/__init__.py +1 -0
- recall/collectors/ai_chat.py +644 -0
- recall/collectors/containers.py +164 -0
- recall/collectors/git.py +540 -0
- recall/collectors/linux_process.py +230 -0
- recall/collectors/linux_session.py +229 -0
- recall/collectors/linux_window.py +199 -0
- recall/collectors/shell.py +300 -0
- recall/collectors/vscode.py +175 -0
- recall/config.py +257 -0
- recall/daemon.py +466 -0
- recall/daemon_main.py +25 -0
- recall/mcp_server.py +290 -0
- recall/models.py +225 -0
- recall/processor/__init__.py +1 -0
- recall/processor/embedder.py +213 -0
- recall/processor/enricher.py +213 -0
- recall/processor/session.py +142 -0
- recall/query/__init__.py +1 -0
- recall/query/context.py +130 -0
- recall/query/llm.py +85 -0
- recall/query/retriever.py +147 -0
- recall/query/timeparser.py +188 -0
- recall/storage/__init__.py +1 -0
- recall/storage/db.py +528 -0
- recall/storage/vectors.py +166 -0
recall/config.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Recall configuration — load, save, and default settings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from platformdirs import user_config_dir, user_data_dir
|
|
11
|
+
|
|
12
|
+
_APP_NAME = "dev-recall"
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Defaults
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
DEFAULT_CONFIG: dict[str, Any] = {
|
|
19
|
+
"data_dir": str(Path(user_data_dir(_APP_NAME))),
|
|
20
|
+
"config_dir": str(Path(user_config_dir(_APP_NAME))),
|
|
21
|
+
"daemon_port": 27182,
|
|
22
|
+
"embedding_model": "all-MiniLM-L6-v2",
|
|
23
|
+
"embedding_dim": 384,
|
|
24
|
+
"llm_model": "anthropic/claude-sonnet-4",
|
|
25
|
+
"retention_days": 90,
|
|
26
|
+
"capture": {
|
|
27
|
+
"terminal": True,
|
|
28
|
+
"git": True,
|
|
29
|
+
"vscode": True,
|
|
30
|
+
"ai_chat": True,
|
|
31
|
+
},
|
|
32
|
+
"privacy": {
|
|
33
|
+
"cmd_ignore_patterns": [
|
|
34
|
+
"*password*",
|
|
35
|
+
"*passwd*",
|
|
36
|
+
"*secret*",
|
|
37
|
+
"*token*",
|
|
38
|
+
"*apikey*",
|
|
39
|
+
"*api_key*",
|
|
40
|
+
"sudo *",
|
|
41
|
+
"*credential*",
|
|
42
|
+
],
|
|
43
|
+
"file_ignore_patterns": [
|
|
44
|
+
"*.env",
|
|
45
|
+
"*.pem",
|
|
46
|
+
"*.key",
|
|
47
|
+
"*.p12",
|
|
48
|
+
".env.*",
|
|
49
|
+
],
|
|
50
|
+
"repo_ignore_patterns": [],
|
|
51
|
+
"ai_chat_max_chars": 200,
|
|
52
|
+
},
|
|
53
|
+
"summary": {
|
|
54
|
+
"auto_generate": True,
|
|
55
|
+
"time": "23:30",
|
|
56
|
+
},
|
|
57
|
+
"search": {
|
|
58
|
+
"default_top_k": 10,
|
|
59
|
+
"rrf_k": 60,
|
|
60
|
+
"session_idle_minutes": 30,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Config class
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Config:
|
|
71
|
+
"""Loaded and saveable configuration."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, data: dict[str, Any]) -> None:
|
|
74
|
+
self._data: dict[str, Any] = data
|
|
75
|
+
|
|
76
|
+
# ------------------------------------------------------------------
|
|
77
|
+
# Top-level properties
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def data_dir(self) -> Path:
|
|
82
|
+
return Path(self._data["data_dir"]).expanduser()
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def config_dir(self) -> Path:
|
|
86
|
+
return Path(self._data["config_dir"]).expanduser()
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def daemon_port(self) -> int:
|
|
90
|
+
return int(self._data.get("daemon_port", 27182))
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def embedding_model(self) -> str:
|
|
94
|
+
return str(self._data.get("embedding_model", "all-MiniLM-L6-v2"))
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def embedding_dim(self) -> int:
|
|
98
|
+
return int(self._data.get("embedding_dim", 384))
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def llm_model(self) -> str:
|
|
102
|
+
return str(self._data.get("llm_model", "anthropic/claude-sonnet-4"))
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def retention_days(self) -> int:
|
|
106
|
+
return int(self._data.get("retention_days", 90))
|
|
107
|
+
|
|
108
|
+
# Nested sections as plain dicts (read-only convenience)
|
|
109
|
+
@property
|
|
110
|
+
def capture(self) -> dict[str, Any]:
|
|
111
|
+
return dict(self._data.get("capture", {}))
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def privacy(self) -> dict[str, Any]:
|
|
115
|
+
return dict(self._data.get("privacy", {}))
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def summary(self) -> dict[str, Any]:
|
|
119
|
+
return dict(self._data.get("summary", {}))
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def search(self) -> dict[str, Any]:
|
|
123
|
+
return dict(self._data.get("search", {}))
|
|
124
|
+
|
|
125
|
+
# ------------------------------------------------------------------
|
|
126
|
+
# Derived paths
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def db_path(self) -> Path:
|
|
131
|
+
return self.data_dir / "events.db"
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def faiss_path(self) -> Path:
|
|
135
|
+
return self.data_dir / "vectors.faiss"
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def shell_tsv_path(self) -> Path:
|
|
139
|
+
return self.data_dir / "shell.tsv"
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def git_tsv_path(self) -> Path:
|
|
143
|
+
return self.data_dir / "git.tsv"
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def pid_path(self) -> Path:
|
|
147
|
+
return self.data_dir / "daemon.pid"
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def log_path(self) -> Path:
|
|
151
|
+
return self.data_dir / "daemon.log"
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def hook_zsh_path(self) -> Path:
|
|
155
|
+
return self.config_dir / "hook.zsh"
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def hook_bash_path(self) -> Path:
|
|
159
|
+
return self.config_dir / "hook.bash"
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def hook_fish_path(self) -> Path:
|
|
163
|
+
return self.config_dir / "hook.fish"
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def git_hooks_dir(self) -> Path:
|
|
167
|
+
return self.config_dir / "git-hooks"
|
|
168
|
+
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
# Get / set by dot-path key (e.g. "privacy.retention_days")
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
def get(self, key: str) -> Any:
|
|
174
|
+
parts = key.split(".")
|
|
175
|
+
node: Any = self._data
|
|
176
|
+
for part in parts:
|
|
177
|
+
if not isinstance(node, dict) or part not in node:
|
|
178
|
+
return None
|
|
179
|
+
node = node[part]
|
|
180
|
+
return node
|
|
181
|
+
|
|
182
|
+
def set(self, key: str, value: Any) -> None:
|
|
183
|
+
parts = key.split(".")
|
|
184
|
+
node = self._data
|
|
185
|
+
for part in parts[:-1]:
|
|
186
|
+
if part not in node or not isinstance(node[part], dict):
|
|
187
|
+
node[part] = {}
|
|
188
|
+
node = node[part]
|
|
189
|
+
# Attempt to coerce type to match existing value
|
|
190
|
+
leaf = parts[-1]
|
|
191
|
+
if leaf in node and node[leaf] is not None:
|
|
192
|
+
existing = node[leaf]
|
|
193
|
+
try:
|
|
194
|
+
if isinstance(existing, bool):
|
|
195
|
+
value = value.lower() in ("true", "1", "yes") if isinstance(value, str) else bool(value)
|
|
196
|
+
elif isinstance(existing, int):
|
|
197
|
+
value = int(value)
|
|
198
|
+
elif isinstance(existing, float):
|
|
199
|
+
value = float(value)
|
|
200
|
+
except (ValueError, AttributeError):
|
|
201
|
+
pass
|
|
202
|
+
node[leaf] = value
|
|
203
|
+
|
|
204
|
+
def as_dict(self) -> dict[str, Any]:
|
|
205
|
+
return dict(self._data)
|
|
206
|
+
|
|
207
|
+
def __repr__(self) -> str:
|
|
208
|
+
return f"Config(data_dir={self.data_dir}, config_dir={self.config_dir})"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# Load / save helpers
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _config_file_path() -> Path:
|
|
217
|
+
"""Return the path to the config JSON file, respecting DEV_RECALL_CONFIG env override."""
|
|
218
|
+
env_path = os.environ.get("DEV_RECALL_CONFIG")
|
|
219
|
+
if env_path:
|
|
220
|
+
return Path(env_path).expanduser()
|
|
221
|
+
return Path(user_config_dir(_APP_NAME)) / "config.json"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def load_config() -> Config:
|
|
225
|
+
"""Load config from disk, merging with defaults for missing keys."""
|
|
226
|
+
path = _config_file_path()
|
|
227
|
+
if not path.exists():
|
|
228
|
+
return Config(_deep_merge({}, DEFAULT_CONFIG))
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
with path.open("r", encoding="utf-8") as fh:
|
|
232
|
+
user_data = json.load(fh)
|
|
233
|
+
except (json.JSONDecodeError, OSError):
|
|
234
|
+
user_data = {}
|
|
235
|
+
|
|
236
|
+
merged = _deep_merge(user_data, DEFAULT_CONFIG)
|
|
237
|
+
return Config(merged)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def save_config(config: Config) -> None:
|
|
241
|
+
"""Persist config to disk."""
|
|
242
|
+
path = _config_file_path()
|
|
243
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
244
|
+
with path.open("w", encoding="utf-8") as fh:
|
|
245
|
+
json.dump(config.as_dict(), fh, indent=2)
|
|
246
|
+
fh.write("\n")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _deep_merge(user: dict, defaults: dict) -> dict:
|
|
250
|
+
"""Return a new dict that is *defaults* overlaid with *user* values (deep)."""
|
|
251
|
+
result = dict(defaults)
|
|
252
|
+
for k, v in user.items():
|
|
253
|
+
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
|
|
254
|
+
result[k] = _deep_merge(v, result[k])
|
|
255
|
+
else:
|
|
256
|
+
result[k] = v
|
|
257
|
+
return result
|