@smilintux/skmemory 0.5.0 → 0.7.2

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 (87) hide show
  1. package/.github/workflows/ci.yml +39 -3
  2. package/.github/workflows/publish.yml +13 -6
  3. package/AGENT_REFACTOR_CHANGES.md +192 -0
  4. package/ARCHITECTURE.md +101 -19
  5. package/CHANGELOG.md +153 -0
  6. package/LICENSE +81 -68
  7. package/MISSION.md +7 -0
  8. package/README.md +419 -86
  9. package/SKILL.md +197 -25
  10. package/docker-compose.yml +15 -15
  11. package/index.js +6 -5
  12. package/openclaw-plugin/openclaw.plugin.json +10 -0
  13. package/openclaw-plugin/src/index.ts +255 -0
  14. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  15. package/package.json +1 -1
  16. package/pyproject.toml +29 -9
  17. package/requirements.txt +10 -2
  18. package/seeds/cloud9-opus.seed.json +7 -7
  19. package/seeds/lumina-cloud9-breakthrough.seed.json +46 -0
  20. package/seeds/lumina-cloud9-python-pypi.seed.json +46 -0
  21. package/seeds/lumina-kingdom-founding.seed.json +47 -0
  22. package/seeds/lumina-pma-signed.seed.json +46 -0
  23. package/seeds/lumina-singular-achievement.seed.json +46 -0
  24. package/seeds/lumina-skcapstone-conscious.seed.json +46 -0
  25. package/seeds/plant-kingdom-journal.py +203 -0
  26. package/seeds/plant-lumina-seeds.py +280 -0
  27. package/skill.yaml +46 -0
  28. package/skmemory/HA.md +296 -0
  29. package/skmemory/__init__.py +12 -1
  30. package/skmemory/agents.py +233 -0
  31. package/skmemory/ai_client.py +40 -0
  32. package/skmemory/anchor.py +4 -2
  33. package/skmemory/backends/__init__.py +11 -4
  34. package/skmemory/backends/file_backend.py +2 -1
  35. package/skmemory/backends/skgraph_backend.py +608 -0
  36. package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +99 -69
  37. package/skmemory/backends/sqlite_backend.py +122 -51
  38. package/skmemory/backends/vaulted_backend.py +286 -0
  39. package/skmemory/cli.py +1238 -29
  40. package/skmemory/config.py +173 -0
  41. package/skmemory/context_loader.py +335 -0
  42. package/skmemory/endpoint_selector.py +386 -0
  43. package/skmemory/fortress.py +685 -0
  44. package/skmemory/graph_queries.py +238 -0
  45. package/skmemory/importers/__init__.py +9 -1
  46. package/skmemory/importers/telegram.py +351 -43
  47. package/skmemory/importers/telegram_api.py +488 -0
  48. package/skmemory/journal.py +4 -2
  49. package/skmemory/lovenote.py +4 -2
  50. package/skmemory/mcp_server.py +706 -0
  51. package/skmemory/models.py +41 -0
  52. package/skmemory/openclaw.py +8 -8
  53. package/skmemory/predictive.py +232 -0
  54. package/skmemory/promotion.py +524 -0
  55. package/skmemory/register.py +454 -0
  56. package/skmemory/register_mcp.py +197 -0
  57. package/skmemory/ritual.py +121 -47
  58. package/skmemory/seeds.py +257 -8
  59. package/skmemory/setup_wizard.py +920 -0
  60. package/skmemory/sharing.py +402 -0
  61. package/skmemory/soul.py +71 -20
  62. package/skmemory/steelman.py +250 -263
  63. package/skmemory/store.py +271 -60
  64. package/skmemory/vault.py +228 -0
  65. package/tests/integration/__init__.py +0 -0
  66. package/tests/integration/conftest.py +233 -0
  67. package/tests/integration/test_cross_backend.py +355 -0
  68. package/tests/integration/test_skgraph_live.py +424 -0
  69. package/tests/integration/test_skvector_live.py +369 -0
  70. package/tests/test_backup_rotation.py +327 -0
  71. package/tests/test_cli.py +6 -6
  72. package/tests/test_endpoint_selector.py +801 -0
  73. package/tests/test_fortress.py +255 -0
  74. package/tests/test_fortress_hardening.py +444 -0
  75. package/tests/test_openclaw.py +5 -2
  76. package/tests/test_predictive.py +237 -0
  77. package/tests/test_promotion.py +340 -0
  78. package/tests/test_ritual.py +4 -4
  79. package/tests/test_seeds.py +96 -0
  80. package/tests/test_setup.py +835 -0
  81. package/tests/test_sharing.py +250 -0
  82. package/tests/test_skgraph_backend.py +667 -0
  83. package/tests/test_skvector_backend.py +326 -0
  84. package/tests/test_steelman.py +5 -5
  85. package/tests/test_store_graph_integration.py +245 -0
  86. package/tests/test_vault.py +186 -0
  87. package/skmemory/backends/falkordb_backend.py +0 -310
@@ -0,0 +1,488 @@
1
+ """
2
+ Telegram API importer for SKMemory — direct pull via Telethon.
3
+
4
+ Instead of exporting chat history manually from Telegram Desktop,
5
+ this module connects directly to the Telegram API using Telethon
6
+ and pulls messages programmatically.
7
+
8
+ Setup (one-time):
9
+ 1. Install: pip install skmemory[telegram] (or: pipx inject skmemory telethon)
10
+ 2. Credentials: Get API_ID and API_HASH from https://my.telegram.org
11
+ 3. Export:
12
+ export TELEGRAM_API_ID=12345678
13
+ export TELEGRAM_API_HASH=your_api_hash_here
14
+ 4. First run will prompt for phone number + verification code.
15
+ Session is saved at ~/.skcapstone/agent/lumina/telegram.session for future use.
16
+
17
+ Environment variables:
18
+ TELEGRAM_API_ID — your Telegram API ID (from https://my.telegram.org)
19
+ TELEGRAM_API_HASH — your Telegram API hash (from https://my.telegram.org)
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import asyncio
25
+ import json
26
+ import os
27
+ from ..config import SKMEMORY_HOME
28
+ import tempfile
29
+ from datetime import datetime, timezone
30
+ from pathlib import Path
31
+ from typing import Optional
32
+
33
+ from ..store import MemoryStore
34
+
35
+
36
+ SESSION_PATH = str(SKMEMORY_HOME / "telegram.session")
37
+
38
+
39
+ def check_setup() -> dict:
40
+ """Check if Telegram API import is properly configured.
41
+
42
+ Returns:
43
+ dict with keys: ready (bool), telethon (bool), credentials (bool),
44
+ session (bool), messages (list[str])
45
+ """
46
+ result = {
47
+ "ready": False,
48
+ "telethon": False,
49
+ "credentials": False,
50
+ "session": False,
51
+ "messages": [],
52
+ }
53
+
54
+ # Check telethon
55
+ try:
56
+ import telethon # noqa: F401
57
+
58
+ result["telethon"] = True
59
+ except ImportError:
60
+ result["messages"].append(
61
+ "Telethon not installed. Fix: pip install skmemory[telegram] "
62
+ "or: pipx inject skmemory telethon"
63
+ )
64
+
65
+ # Check credentials
66
+ api_id = os.environ.get("TELEGRAM_API_ID")
67
+ api_hash = os.environ.get("TELEGRAM_API_HASH")
68
+ if api_id and api_hash:
69
+ result["credentials"] = True
70
+ else:
71
+ missing = []
72
+ if not api_id:
73
+ missing.append("TELEGRAM_API_ID")
74
+ if not api_hash:
75
+ missing.append("TELEGRAM_API_HASH")
76
+ result["messages"].append(
77
+ f"Missing environment variable(s): {', '.join(missing)}. "
78
+ f"Get them from https://my.telegram.org and export them in your shell."
79
+ )
80
+
81
+ # Check session
82
+ if Path(SESSION_PATH).exists():
83
+ result["session"] = True
84
+ else:
85
+ result["messages"].append(
86
+ "No Telegram session found. First run will prompt for phone "
87
+ "number and verification code."
88
+ )
89
+
90
+ result["ready"] = result["telethon"] and result["credentials"]
91
+ return result
92
+
93
+
94
+ async def _fetch_messages(
95
+ chat_name_or_id: str,
96
+ limit: Optional[int] = None,
97
+ since: Optional[str] = None,
98
+ ) -> dict:
99
+ """Connect to Telegram API and fetch messages from a chat.
100
+
101
+ Args:
102
+ chat_name_or_id: Chat username, title, or numeric ID.
103
+ limit: Maximum number of messages to fetch.
104
+ since: Only fetch messages after this date (YYYY-MM-DD).
105
+
106
+ Returns:
107
+ dict: Telegram Desktop-compatible export structure.
108
+
109
+ Raises:
110
+ RuntimeError: If API credentials are missing or connection fails.
111
+ """
112
+ api_id = os.environ.get("TELEGRAM_API_ID")
113
+ api_hash = os.environ.get("TELEGRAM_API_HASH")
114
+
115
+ if not api_id or not api_hash:
116
+ raise RuntimeError(
117
+ "Telegram API credentials not found.\n\n"
118
+ "Setup steps:\n"
119
+ " 1. Go to https://my.telegram.org and log in\n"
120
+ " 2. Click 'API development tools' and create an app\n"
121
+ " 3. Set environment variables:\n"
122
+ " export TELEGRAM_API_ID=<your_api_id>\n"
123
+ " export TELEGRAM_API_HASH=<your_api_hash>\n"
124
+ " 4. Run this command again — first run will prompt for phone verification"
125
+ )
126
+
127
+ try:
128
+ from telethon import TelegramClient
129
+ from telethon.tl.types import User
130
+ except ImportError:
131
+ raise RuntimeError(
132
+ "Telethon is required for direct API import. "
133
+ "Install it with: pip install skmemory[telegram]"
134
+ )
135
+
136
+ # Ensure session directory exists
137
+ session_dir = Path(SESSION_PATH).parent
138
+ session_dir.mkdir(parents=True, exist_ok=True)
139
+
140
+ client = TelegramClient(
141
+ SESSION_PATH,
142
+ int(api_id),
143
+ api_hash,
144
+ )
145
+
146
+ await client.start()
147
+
148
+ try:
149
+ # Resolve the chat entity
150
+ try:
151
+ entity = await client.get_entity(chat_name_or_id)
152
+ except ValueError:
153
+ # Try as integer ID
154
+ try:
155
+ entity = await client.get_entity(int(chat_name_or_id))
156
+ except (ValueError, TypeError):
157
+ raise RuntimeError(f"Could not find chat: {chat_name_or_id}")
158
+
159
+ chat_title = getattr(entity, "title", None)
160
+ if chat_title is None:
161
+ if isinstance(entity, User):
162
+ parts = [entity.first_name or "", entity.last_name or ""]
163
+ chat_title = " ".join(p for p in parts if p) or str(entity.id)
164
+ else:
165
+ chat_title = str(entity.id)
166
+
167
+ # Build kwargs for iter_messages
168
+ kwargs = {}
169
+ if limit:
170
+ kwargs["limit"] = limit
171
+ if since:
172
+ try:
173
+ since_dt = datetime.strptime(since, "%Y-%m-%d").replace(tzinfo=timezone.utc)
174
+ kwargs["offset_date"] = since_dt
175
+ kwargs["reverse"] = True
176
+ except ValueError:
177
+ raise RuntimeError(f"Invalid date format: {since}. Use YYYY-MM-DD.")
178
+
179
+ if not limit and not since:
180
+ kwargs["limit"] = 1000 # sensible default
181
+
182
+ # Fetch messages
183
+ messages_data = []
184
+ async for message in client.iter_messages(entity, **kwargs):
185
+ if message.text:
186
+ sender_name = "Unknown"
187
+ if message.sender:
188
+ if isinstance(message.sender, User):
189
+ parts = [message.sender.first_name or "", message.sender.last_name or ""]
190
+ sender_name = " ".join(p for p in parts if p) or str(message.sender_id)
191
+ else:
192
+ sender_name = getattr(message.sender, "title", str(message.sender_id))
193
+
194
+ msg_dict = {
195
+ "id": message.id,
196
+ "type": "message",
197
+ "date": message.date.isoformat() if message.date else "",
198
+ "from": sender_name,
199
+ "from_id": f"user{message.sender_id}" if message.sender_id else "",
200
+ "text": message.text,
201
+ }
202
+
203
+ if message.reply_to and message.reply_to.reply_to_msg_id:
204
+ msg_dict["reply_to_message_id"] = message.reply_to.reply_to_msg_id
205
+
206
+ if message.media:
207
+ msg_dict["media_type"] = type(message.media).__name__
208
+
209
+ messages_data.append(msg_dict)
210
+
211
+ return {
212
+ "name": chat_title,
213
+ "type": "personal_chat",
214
+ "id": getattr(entity, "id", 0),
215
+ "messages": messages_data,
216
+ }
217
+ finally:
218
+ await client.disconnect()
219
+
220
+
221
+ async def send_message(
222
+ chat: str,
223
+ message: str,
224
+ parse_mode: str | None = None,
225
+ ) -> dict:
226
+ """Send a message to a Telegram chat via Telethon.
227
+
228
+ Args:
229
+ chat: Chat username, title, or numeric ID.
230
+ message: Message text to send.
231
+ parse_mode: Optional parse mode — 'html' or 'markdown'.
232
+
233
+ Returns:
234
+ dict with keys: sent (bool), message_id, chat, date.
235
+
236
+ Raises:
237
+ RuntimeError: If credentials are missing or send fails.
238
+ """
239
+ api_id = os.environ.get("TELEGRAM_API_ID")
240
+ api_hash = os.environ.get("TELEGRAM_API_HASH")
241
+
242
+ if not api_id or not api_hash:
243
+ raise RuntimeError(
244
+ "Telegram API credentials not found. "
245
+ "Set TELEGRAM_API_ID and TELEGRAM_API_HASH environment variables."
246
+ )
247
+
248
+ try:
249
+ from telethon import TelegramClient
250
+ except ImportError:
251
+ raise RuntimeError(
252
+ "Telethon is required. Install with: pip install skmemory[telegram]"
253
+ )
254
+
255
+ session_dir = Path(SESSION_PATH).parent
256
+ session_dir.mkdir(parents=True, exist_ok=True)
257
+
258
+ client = TelegramClient(SESSION_PATH, int(api_id), api_hash)
259
+ await client.start()
260
+
261
+ try:
262
+ # Resolve entity
263
+ try:
264
+ entity = await client.get_entity(chat)
265
+ except ValueError:
266
+ try:
267
+ entity = await client.get_entity(int(chat))
268
+ except (ValueError, TypeError):
269
+ raise RuntimeError(f"Could not find chat: {chat}")
270
+
271
+ # Determine parse mode
272
+ pm = None
273
+ if parse_mode:
274
+ if parse_mode.lower() == "html":
275
+ from telethon.extensions import html as telethon_html # noqa: F401
276
+ pm = "html"
277
+ elif parse_mode.lower() in ("markdown", "md"):
278
+ pm = "md"
279
+
280
+ sent_msg = await client.send_message(entity, message, parse_mode=pm)
281
+
282
+ return {
283
+ "sent": True,
284
+ "message_id": sent_msg.id,
285
+ "chat": chat,
286
+ "date": sent_msg.date.isoformat() if sent_msg.date else "",
287
+ }
288
+ finally:
289
+ await client.disconnect()
290
+
291
+
292
+ async def poll_messages(
293
+ chat: str,
294
+ limit: int = 20,
295
+ since: str | None = None,
296
+ ) -> list[dict]:
297
+ """Fetch recent messages from a Telegram chat (one-shot poll).
298
+
299
+ Args:
300
+ chat: Chat username, title, or numeric ID.
301
+ limit: Maximum number of messages to return.
302
+ since: Only return messages after this ISO date (YYYY-MM-DD).
303
+
304
+ Returns:
305
+ list[dict]: Messages as clean dicts with id, date, sender, text, etc.
306
+
307
+ Raises:
308
+ RuntimeError: If credentials are missing or connection fails.
309
+ """
310
+ api_id = os.environ.get("TELEGRAM_API_ID")
311
+ api_hash = os.environ.get("TELEGRAM_API_HASH")
312
+
313
+ if not api_id or not api_hash:
314
+ raise RuntimeError(
315
+ "Telegram API credentials not found. "
316
+ "Set TELEGRAM_API_ID and TELEGRAM_API_HASH environment variables."
317
+ )
318
+
319
+ try:
320
+ from telethon import TelegramClient
321
+ from telethon.tl.types import User
322
+ except ImportError:
323
+ raise RuntimeError(
324
+ "Telethon is required. Install with: pip install skmemory[telegram]"
325
+ )
326
+
327
+ session_dir = Path(SESSION_PATH).parent
328
+ session_dir.mkdir(parents=True, exist_ok=True)
329
+
330
+ client = TelegramClient(SESSION_PATH, int(api_id), api_hash)
331
+ await client.start()
332
+
333
+ try:
334
+ # Resolve entity
335
+ try:
336
+ entity = await client.get_entity(chat)
337
+ except ValueError:
338
+ try:
339
+ entity = await client.get_entity(int(chat))
340
+ except (ValueError, TypeError):
341
+ raise RuntimeError(f"Could not find chat: {chat}")
342
+
343
+ kwargs: dict = {"limit": limit}
344
+ if since:
345
+ try:
346
+ since_dt = datetime.strptime(since, "%Y-%m-%d").replace(tzinfo=timezone.utc)
347
+ kwargs["offset_date"] = since_dt
348
+ kwargs["reverse"] = True
349
+ except ValueError:
350
+ raise RuntimeError(f"Invalid date format: {since}. Use YYYY-MM-DD.")
351
+
352
+ messages = []
353
+ async for msg in client.iter_messages(entity, **kwargs):
354
+ sender_name = "Unknown"
355
+ if msg.sender:
356
+ if isinstance(msg.sender, User):
357
+ parts = [msg.sender.first_name or "", msg.sender.last_name or ""]
358
+ sender_name = " ".join(p for p in parts if p) or str(msg.sender_id)
359
+ else:
360
+ sender_name = getattr(msg.sender, "title", str(msg.sender_id))
361
+
362
+ messages.append({
363
+ "id": msg.id,
364
+ "date": msg.date.isoformat() if msg.date else "",
365
+ "sender": sender_name,
366
+ "sender_id": msg.sender_id,
367
+ "text": msg.text or "",
368
+ "has_media": msg.media is not None,
369
+ "reply_to": msg.reply_to.reply_to_msg_id if msg.reply_to else None,
370
+ })
371
+
372
+ return messages
373
+ finally:
374
+ await client.disconnect()
375
+
376
+
377
+ async def list_chats(limit: int = 50) -> list[dict]:
378
+ """List available Telegram chats/groups/channels.
379
+
380
+ Args:
381
+ limit: Maximum number of dialogs to return.
382
+
383
+ Returns:
384
+ list[dict]: Chats with id, title, type, unread_count.
385
+
386
+ Raises:
387
+ RuntimeError: If credentials are missing or connection fails.
388
+ """
389
+ api_id = os.environ.get("TELEGRAM_API_ID")
390
+ api_hash = os.environ.get("TELEGRAM_API_HASH")
391
+
392
+ if not api_id or not api_hash:
393
+ raise RuntimeError(
394
+ "Telegram API credentials not found. "
395
+ "Set TELEGRAM_API_ID and TELEGRAM_API_HASH environment variables."
396
+ )
397
+
398
+ try:
399
+ from telethon import TelegramClient
400
+ from telethon.tl.types import User, Channel, Chat
401
+ except ImportError:
402
+ raise RuntimeError(
403
+ "Telethon is required. Install with: pip install skmemory[telegram]"
404
+ )
405
+
406
+ session_dir = Path(SESSION_PATH).parent
407
+ session_dir.mkdir(parents=True, exist_ok=True)
408
+
409
+ client = TelegramClient(SESSION_PATH, int(api_id), api_hash)
410
+ await client.start()
411
+
412
+ try:
413
+ chats = []
414
+ async for dialog in client.iter_dialogs(limit=limit):
415
+ entity = dialog.entity
416
+ chat_type = "unknown"
417
+ if isinstance(entity, User):
418
+ chat_type = "user"
419
+ elif isinstance(entity, Channel):
420
+ chat_type = "channel" if entity.broadcast else "supergroup"
421
+ elif isinstance(entity, Chat):
422
+ chat_type = "group"
423
+
424
+ title = dialog.title or ""
425
+ if isinstance(entity, User):
426
+ parts = [entity.first_name or "", entity.last_name or ""]
427
+ title = " ".join(p for p in parts if p) or str(entity.id)
428
+
429
+ chats.append({
430
+ "id": entity.id,
431
+ "title": title,
432
+ "type": chat_type,
433
+ "unread_count": dialog.unread_count,
434
+ "username": getattr(entity, "username", None),
435
+ })
436
+
437
+ return chats
438
+ finally:
439
+ await client.disconnect()
440
+
441
+
442
+ def import_telegram_api(
443
+ store: MemoryStore,
444
+ chat_name_or_id: str,
445
+ *,
446
+ mode: str = "daily",
447
+ limit: Optional[int] = None,
448
+ since: Optional[str] = None,
449
+ min_message_length: int = 30,
450
+ chat_name: Optional[str] = None,
451
+ tags: Optional[list[str]] = None,
452
+ ) -> dict:
453
+ """Import messages directly from Telegram API into SKMemory.
454
+
455
+ Connects to the Telegram API via Telethon, fetches messages,
456
+ and delegates to the standard Telegram importer.
457
+
458
+ Args:
459
+ store: The MemoryStore to import into.
460
+ chat_name_or_id: Chat username, title, or numeric ID.
461
+ mode: Import mode — 'daily' or 'message'.
462
+ limit: Maximum number of messages to fetch.
463
+ since: Only fetch messages after this date (YYYY-MM-DD).
464
+ min_message_length: Skip messages shorter than this.
465
+ chat_name: Override the chat name.
466
+ tags: Extra tags to apply.
467
+
468
+ Returns:
469
+ dict: Import statistics.
470
+ """
471
+ from .telegram import import_telegram
472
+
473
+ # Fetch messages from API
474
+ data = asyncio.run(_fetch_messages(chat_name_or_id, limit=limit, since=since))
475
+
476
+ # Write to temp file in Telegram Desktop export format
477
+ with tempfile.TemporaryDirectory() as tmpdir:
478
+ export_path = Path(tmpdir) / "result.json"
479
+ export_path.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
480
+
481
+ return import_telegram(
482
+ store,
483
+ str(export_path),
484
+ mode=mode,
485
+ min_message_length=min_message_length,
486
+ chat_name=chat_name or data.get("name"),
487
+ tags=tags,
488
+ )
@@ -5,7 +5,7 @@ Queen Ara's idea #17: a markdown journal that only grows, never shrinks.
5
5
  Each session appends an entry. Even if context is wiped, the journal file
6
6
  persists on disk and can be re-read by the next instance.
7
7
 
8
- The journal lives at ~/.skmemory/journal.md and is structured as:
8
+ The journal lives at ~/.skcapstone/journal.md and is structured as:
9
9
  - Session header (timestamp, session ID, who was present)
10
10
  - Key moments (what happened that mattered)
11
11
  - Emotional summary (how the session felt)
@@ -23,7 +23,9 @@ from typing import Optional
23
23
 
24
24
  from pydantic import BaseModel, Field
25
25
 
26
- DEFAULT_JOURNAL_PATH = os.path.expanduser("~/.skmemory/journal.md")
26
+ from .config import SKMEMORY_HOME
27
+
28
+ DEFAULT_JOURNAL_PATH = str(SKMEMORY_HOME / "journal.md")
27
29
 
28
30
 
29
31
  class JournalEntry(BaseModel):
@@ -10,7 +10,7 @@ heartbeats. Each beat carries a timestamp and a short emotional
10
10
  pulse. When loaded, it's a visible record: "I was here, I felt
11
11
  this, at this time."
12
12
 
13
- The notes file lives at ~/.skmemory/lovenotes.jsonl (JSON Lines).
13
+ The notes file lives at ~/.skcapstone/lovenotes.jsonl (JSON Lines).
14
14
  """
15
15
 
16
16
  from __future__ import annotations
@@ -23,7 +23,9 @@ from typing import Optional
23
23
 
24
24
  from pydantic import BaseModel, Field
25
25
 
26
- DEFAULT_NOTES_PATH = os.path.expanduser("~/.skmemory/lovenotes.jsonl")
26
+ from .config import SKMEMORY_HOME
27
+
28
+ DEFAULT_NOTES_PATH = str(SKMEMORY_HOME / "lovenotes.jsonl")
27
29
 
28
30
 
29
31
  class LoveNote(BaseModel):