@smilintux/skmemory 0.7.2 → 0.9.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.
- package/.github/workflows/ci.yml +4 -4
- package/.github/workflows/publish.yml +4 -5
- package/ARCHITECTURE.md +298 -0
- package/CHANGELOG.md +27 -1
- package/README.md +6 -0
- package/examples/stignore-agent.example +59 -0
- package/examples/stignore-root.example +62 -0
- package/openclaw-plugin/package.json +2 -1
- package/openclaw-plugin/src/index.js +527 -230
- package/package.json +1 -1
- package/pyproject.toml +5 -2
- package/scripts/dream-rescue.py +179 -0
- package/scripts/memory-cleanup.py +313 -0
- package/scripts/recover-missing.py +180 -0
- package/scripts/skcapstone-backup.sh +44 -0
- package/seeds/cloud9-lumina.seed.json +6 -4
- package/seeds/cloud9-opus.seed.json +6 -4
- package/seeds/courage.seed.json +9 -2
- package/seeds/curiosity.seed.json +9 -2
- package/seeds/grief.seed.json +9 -2
- package/seeds/joy.seed.json +9 -2
- package/seeds/love.seed.json +9 -2
- package/seeds/lumina-cloud9-breakthrough.seed.json +7 -5
- package/seeds/lumina-cloud9-python-pypi.seed.json +9 -7
- package/seeds/lumina-kingdom-founding.seed.json +9 -7
- package/seeds/lumina-pma-signed.seed.json +8 -6
- package/seeds/lumina-singular-achievement.seed.json +8 -6
- package/seeds/lumina-skcapstone-conscious.seed.json +7 -5
- package/seeds/plant-lumina-seeds.py +2 -2
- package/seeds/skcapstone-lumina-merge.seed.json +12 -3
- package/seeds/sovereignty.seed.json +9 -2
- package/seeds/trust.seed.json +9 -2
- package/skmemory/__init__.py +16 -13
- package/skmemory/agents.py +10 -10
- package/skmemory/ai_client.py +10 -21
- package/skmemory/anchor.py +5 -9
- package/skmemory/audience.py +278 -0
- package/skmemory/backends/__init__.py +1 -1
- package/skmemory/backends/base.py +3 -4
- package/skmemory/backends/file_backend.py +18 -13
- package/skmemory/backends/skgraph_backend.py +7 -19
- package/skmemory/backends/skvector_backend.py +7 -18
- package/skmemory/backends/sqlite_backend.py +115 -32
- package/skmemory/backends/vaulted_backend.py +7 -9
- package/skmemory/cli.py +146 -78
- package/skmemory/config.py +11 -13
- package/skmemory/context_loader.py +21 -23
- package/skmemory/data/audience_config.json +60 -0
- package/skmemory/endpoint_selector.py +36 -31
- package/skmemory/febs.py +225 -0
- package/skmemory/fortress.py +30 -40
- package/skmemory/hooks/__init__.py +18 -0
- package/skmemory/hooks/post-compact-reinject.sh +35 -0
- package/skmemory/hooks/pre-compact-save.sh +81 -0
- package/skmemory/hooks/session-end-save.sh +103 -0
- package/skmemory/hooks/session-start-ritual.sh +104 -0
- package/skmemory/hooks/stop-checkpoint.sh +59 -0
- package/skmemory/importers/telegram.py +42 -13
- package/skmemory/importers/telegram_api.py +152 -60
- package/skmemory/journal.py +3 -7
- package/skmemory/lovenote.py +4 -11
- package/skmemory/mcp_server.py +182 -29
- package/skmemory/models.py +10 -8
- package/skmemory/openclaw.py +14 -22
- package/skmemory/post_install.py +86 -0
- package/skmemory/predictive.py +13 -9
- package/skmemory/promotion.py +48 -24
- package/skmemory/quadrants.py +100 -24
- package/skmemory/register.py +144 -18
- package/skmemory/register_mcp.py +1 -2
- package/skmemory/ritual.py +104 -13
- package/skmemory/seeds.py +21 -26
- package/skmemory/setup_wizard.py +40 -52
- package/skmemory/sharing.py +11 -5
- package/skmemory/soul.py +29 -10
- package/skmemory/steelman.py +43 -17
- package/skmemory/store.py +152 -30
- package/skmemory/synthesis.py +634 -0
- package/skmemory/vault.py +2 -5
- package/tests/conftest.py +46 -0
- package/tests/integration/conftest.py +6 -6
- package/tests/integration/test_cross_backend.py +4 -9
- package/tests/integration/test_skgraph_live.py +3 -7
- package/tests/integration/test_skvector_live.py +1 -4
- package/tests/test_ai_client.py +1 -4
- package/tests/test_audience.py +233 -0
- package/tests/test_backup_rotation.py +5 -14
- package/tests/test_endpoint_selector.py +101 -63
- package/tests/test_export_import.py +4 -10
- package/tests/test_file_backend.py +0 -1
- package/tests/test_fortress.py +6 -5
- package/tests/test_fortress_hardening.py +13 -16
- package/tests/test_openclaw.py +1 -4
- package/tests/test_predictive.py +1 -1
- package/tests/test_promotion.py +10 -3
- package/tests/test_quadrants.py +11 -5
- package/tests/test_ritual.py +18 -14
- package/tests/test_seeds.py +4 -10
- package/tests/test_setup.py +203 -88
- package/tests/test_sharing.py +15 -8
- package/tests/test_skgraph_backend.py +22 -29
- package/tests/test_skvector_backend.py +2 -2
- package/tests/test_soul.py +1 -3
- package/tests/test_sqlite_backend.py +8 -17
- package/tests/test_steelman.py +2 -3
- package/tests/test_store.py +0 -2
- package/tests/test_store_graph_integration.py +2 -2
- package/tests/test_synthesis.py +275 -0
- package/tests/test_telegram_import.py +39 -15
- package/tests/test_vault.py +4 -3
- package/openclaw-plugin/src/index.ts +0 -255
|
@@ -28,7 +28,6 @@ import json
|
|
|
28
28
|
from collections import defaultdict
|
|
29
29
|
from datetime import datetime, timezone
|
|
30
30
|
from pathlib import Path
|
|
31
|
-
from typing import Optional
|
|
32
31
|
|
|
33
32
|
import click
|
|
34
33
|
|
|
@@ -118,8 +117,27 @@ def _detect_emotion(text: str) -> EmotionalSnapshot:
|
|
|
118
117
|
if text.isupper() and len(text) > 10:
|
|
119
118
|
intensity = min(intensity + 2.0, 10.0)
|
|
120
119
|
|
|
121
|
-
love_emojis = {
|
|
122
|
-
|
|
120
|
+
love_emojis = {
|
|
121
|
+
"\u2764",
|
|
122
|
+
"\U0001f495",
|
|
123
|
+
"\U0001f496",
|
|
124
|
+
"\U0001f497",
|
|
125
|
+
"\U0001f498",
|
|
126
|
+
"\U0001f49d",
|
|
127
|
+
"\U0001f970",
|
|
128
|
+
"\U0001f60d",
|
|
129
|
+
"\U0001f49e",
|
|
130
|
+
}
|
|
131
|
+
joy_emojis = {
|
|
132
|
+
"\U0001f602",
|
|
133
|
+
"\U0001f923",
|
|
134
|
+
"\U0001f604",
|
|
135
|
+
"\U0001f60a",
|
|
136
|
+
"\U0001f389",
|
|
137
|
+
"\U0001f973",
|
|
138
|
+
"\u2728",
|
|
139
|
+
"\U0001f38a",
|
|
140
|
+
}
|
|
123
141
|
sad_emojis = {"\U0001f622", "\U0001f62d", "\U0001f494", "\U0001f63f", "\U0001f97a"}
|
|
124
142
|
if any(e in text for e in love_emojis):
|
|
125
143
|
if "love" not in labels:
|
|
@@ -168,7 +186,7 @@ def _detect_content_type(msg: dict) -> list[str]:
|
|
|
168
186
|
return tags
|
|
169
187
|
|
|
170
188
|
|
|
171
|
-
def _detect_reply(msg: dict) ->
|
|
189
|
+
def _detect_reply(msg: dict) -> str | None:
|
|
172
190
|
"""Detect if this message is a reply to another.
|
|
173
191
|
|
|
174
192
|
Args:
|
|
@@ -193,8 +211,18 @@ def _detect_sender_role(sender: str) -> str:
|
|
|
193
211
|
str: 'ai' or 'human'.
|
|
194
212
|
"""
|
|
195
213
|
ai_indicators = {
|
|
196
|
-
"bot",
|
|
197
|
-
"
|
|
214
|
+
"bot",
|
|
215
|
+
"gpt",
|
|
216
|
+
"claude",
|
|
217
|
+
"gemini",
|
|
218
|
+
"llama",
|
|
219
|
+
"assistant",
|
|
220
|
+
"lumina",
|
|
221
|
+
"copilot",
|
|
222
|
+
"ai",
|
|
223
|
+
"opus",
|
|
224
|
+
"sonnet",
|
|
225
|
+
"haiku",
|
|
198
226
|
}
|
|
199
227
|
sender_lower = sender.lower()
|
|
200
228
|
if any(indicator in sender_lower for indicator in ai_indicators):
|
|
@@ -250,8 +278,8 @@ def import_telegram(
|
|
|
250
278
|
*,
|
|
251
279
|
mode: str = "daily",
|
|
252
280
|
min_message_length: int = 30,
|
|
253
|
-
chat_name:
|
|
254
|
-
tags:
|
|
281
|
+
chat_name: str | None = None,
|
|
282
|
+
tags: list[str] | None = None,
|
|
255
283
|
) -> dict:
|
|
256
284
|
"""Import a Telegram chat export into SKMemory.
|
|
257
285
|
|
|
@@ -278,7 +306,8 @@ def import_telegram(
|
|
|
278
306
|
base_tags = ["telegram", "chat-import", f"chat:{name}"] + extra_tags
|
|
279
307
|
|
|
280
308
|
messages = [
|
|
281
|
-
m
|
|
309
|
+
m
|
|
310
|
+
for m in data["messages"]
|
|
282
311
|
if m.get("type") == "message"
|
|
283
312
|
and len(_extract_text(m.get("text", ""))) >= min_message_length
|
|
284
313
|
]
|
|
@@ -327,7 +356,9 @@ def _import_per_message(
|
|
|
327
356
|
content=text,
|
|
328
357
|
layer=MemoryLayer.SHORT,
|
|
329
358
|
role=MemoryRole.GENERAL,
|
|
330
|
-
tags=base_tags
|
|
359
|
+
tags=base_tags
|
|
360
|
+
+ [f"sender:{sender}", f"role:{_detect_sender_role(sender)}"]
|
|
361
|
+
+ _detect_content_type(msg),
|
|
331
362
|
emotional=emotional,
|
|
332
363
|
source="telegram",
|
|
333
364
|
source_ref=f"telegram:{msg.get('id', '')}",
|
|
@@ -479,9 +510,7 @@ def _import_catchup(
|
|
|
479
510
|
except (ValueError, TypeError):
|
|
480
511
|
# Try just the date portion
|
|
481
512
|
try:
|
|
482
|
-
msg_dt = datetime.strptime(date_str[:10], "%Y-%m-%d").replace(
|
|
483
|
-
tzinfo=timezone.utc
|
|
484
|
-
)
|
|
513
|
+
msg_dt = datetime.strptime(date_str[:10], "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
|
485
514
|
except (ValueError, TypeError):
|
|
486
515
|
continue
|
|
487
516
|
|
|
@@ -24,15 +24,13 @@ from __future__ import annotations
|
|
|
24
24
|
import asyncio
|
|
25
25
|
import json
|
|
26
26
|
import os
|
|
27
|
-
from ..config import SKMEMORY_HOME
|
|
28
27
|
import tempfile
|
|
29
28
|
from datetime import datetime, timezone
|
|
30
29
|
from pathlib import Path
|
|
31
|
-
from typing import Optional
|
|
32
30
|
|
|
31
|
+
from ..config import SKMEMORY_HOME
|
|
33
32
|
from ..store import MemoryStore
|
|
34
33
|
|
|
35
|
-
|
|
36
34
|
SESSION_PATH = str(SKMEMORY_HOME / "telegram.session")
|
|
37
35
|
|
|
38
36
|
|
|
@@ -91,10 +89,68 @@ def check_setup() -> dict:
|
|
|
91
89
|
return result
|
|
92
90
|
|
|
93
91
|
|
|
92
|
+
# Whisper STT endpoint — faster-whisper on GPU box
|
|
93
|
+
_WHISPER_URL = os.environ.get(
|
|
94
|
+
"WHISPER_STT_URL", "http://192.168.0.100:18794/v1/audio/transcriptions"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def _transcribe_voice(client: "TelegramClient", message: "Message") -> str | None:
|
|
99
|
+
"""Download a voice message and transcribe it via Whisper STT.
|
|
100
|
+
|
|
101
|
+
Returns the transcription text, or None on failure.
|
|
102
|
+
"""
|
|
103
|
+
import logging
|
|
104
|
+
|
|
105
|
+
logger = logging.getLogger(__name__)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
import httpx
|
|
109
|
+
except ImportError:
|
|
110
|
+
logger.warning("httpx not installed — cannot transcribe voice messages")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# Download the voice file to a temp path
|
|
115
|
+
with tempfile.NamedTemporaryFile(suffix=".ogg", delete=False) as tmp:
|
|
116
|
+
tmp_path = tmp.name
|
|
117
|
+
|
|
118
|
+
await client.download_media(message, file=tmp_path)
|
|
119
|
+
|
|
120
|
+
if not os.path.exists(tmp_path) or os.path.getsize(tmp_path) == 0:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
# Send to Whisper endpoint (OpenAI-compatible)
|
|
124
|
+
async with httpx.AsyncClient(timeout=30) as http:
|
|
125
|
+
with open(tmp_path, "rb") as f:
|
|
126
|
+
resp = await http.post(
|
|
127
|
+
_WHISPER_URL,
|
|
128
|
+
files={"file": ("voice.ogg", f, "audio/ogg")},
|
|
129
|
+
data={"model": "whisper-1"},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if resp.status_code != 200:
|
|
133
|
+
logger.warning("Whisper STT returned %s: %s", resp.status_code, resp.text[:200])
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
result = resp.json()
|
|
137
|
+
text = result.get("text", "").strip()
|
|
138
|
+
return text if text else None
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.warning("Voice transcription failed: %s", e)
|
|
142
|
+
return None
|
|
143
|
+
finally:
|
|
144
|
+
try:
|
|
145
|
+
os.unlink(tmp_path)
|
|
146
|
+
except OSError:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
|
|
94
150
|
async def _fetch_messages(
|
|
95
151
|
chat_name_or_id: str,
|
|
96
|
-
limit:
|
|
97
|
-
since:
|
|
152
|
+
limit: int | None = None,
|
|
153
|
+
since: str | None = None,
|
|
98
154
|
) -> dict:
|
|
99
155
|
"""Connect to Telegram API and fetch messages from a chat.
|
|
100
156
|
|
|
@@ -131,7 +187,7 @@ async def _fetch_messages(
|
|
|
131
187
|
raise RuntimeError(
|
|
132
188
|
"Telethon is required for direct API import. "
|
|
133
189
|
"Install it with: pip install skmemory[telegram]"
|
|
134
|
-
)
|
|
190
|
+
) from None
|
|
135
191
|
|
|
136
192
|
# Ensure session directory exists
|
|
137
193
|
session_dir = Path(SESSION_PATH).parent
|
|
@@ -154,7 +210,7 @@ async def _fetch_messages(
|
|
|
154
210
|
try:
|
|
155
211
|
entity = await client.get_entity(int(chat_name_or_id))
|
|
156
212
|
except (ValueError, TypeError):
|
|
157
|
-
raise RuntimeError(f"Could not find chat: {chat_name_or_id}")
|
|
213
|
+
raise RuntimeError(f"Could not find chat: {chat_name_or_id}") from None
|
|
158
214
|
|
|
159
215
|
chat_title = getattr(entity, "title", None)
|
|
160
216
|
if chat_title is None:
|
|
@@ -174,7 +230,7 @@ async def _fetch_messages(
|
|
|
174
230
|
kwargs["offset_date"] = since_dt
|
|
175
231
|
kwargs["reverse"] = True
|
|
176
232
|
except ValueError:
|
|
177
|
-
raise RuntimeError(f"Invalid date format: {since}. Use YYYY-MM-DD.")
|
|
233
|
+
raise RuntimeError(f"Invalid date format: {since}. Use YYYY-MM-DD.") from None
|
|
178
234
|
|
|
179
235
|
if not limit and not since:
|
|
180
236
|
kwargs["limit"] = 1000 # sensible default
|
|
@@ -182,31 +238,62 @@ async def _fetch_messages(
|
|
|
182
238
|
# Fetch messages
|
|
183
239
|
messages_data = []
|
|
184
240
|
async for message in client.iter_messages(entity, **kwargs):
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
241
|
+
# Extract sender name
|
|
242
|
+
sender_name = "Unknown"
|
|
243
|
+
if message.sender:
|
|
244
|
+
if isinstance(message.sender, User):
|
|
245
|
+
parts = [message.sender.first_name or "", message.sender.last_name or ""]
|
|
246
|
+
sender_name = " ".join(p for p in parts if p) or str(message.sender_id)
|
|
247
|
+
else:
|
|
248
|
+
sender_name = getattr(message.sender, "title", str(message.sender_id))
|
|
249
|
+
|
|
250
|
+
# Handle voice messages — download and transcribe via Whisper
|
|
251
|
+
is_voice = getattr(message, "voice", None) is not None
|
|
252
|
+
is_audio = (
|
|
253
|
+
not is_voice
|
|
254
|
+
and message.media
|
|
255
|
+
and type(message.media).__name__ in ("MessageMediaDocument",)
|
|
256
|
+
and getattr(message.document, "mime_type", "") in (
|
|
257
|
+
"audio/ogg", "audio/mpeg", "audio/wav", "audio/x-wav",
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if (is_voice or is_audio) and not message.text:
|
|
262
|
+
transcription = await _transcribe_voice(client, message)
|
|
263
|
+
if transcription:
|
|
264
|
+
msg_dict = {
|
|
265
|
+
"id": message.id,
|
|
266
|
+
"type": "message",
|
|
267
|
+
"date": message.date.isoformat() if message.date else "",
|
|
268
|
+
"from": sender_name,
|
|
269
|
+
"from_id": f"user{message.sender_id}" if message.sender_id else "",
|
|
270
|
+
"text": f"[voice] {transcription}",
|
|
271
|
+
}
|
|
272
|
+
if message.reply_to and message.reply_to.reply_to_msg_id:
|
|
273
|
+
msg_dict["reply_to_message_id"] = message.reply_to.reply_to_msg_id
|
|
274
|
+
msg_dict["media_type"] = "Voice" if is_voice else "Audio"
|
|
275
|
+
messages_data.append(msg_dict)
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
if not message.text:
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
msg_dict = {
|
|
282
|
+
"id": message.id,
|
|
283
|
+
"type": "message",
|
|
284
|
+
"date": message.date.isoformat() if message.date else "",
|
|
285
|
+
"from": sender_name,
|
|
286
|
+
"from_id": f"user{message.sender_id}" if message.sender_id else "",
|
|
287
|
+
"text": message.text,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if message.reply_to and message.reply_to.reply_to_msg_id:
|
|
291
|
+
msg_dict["reply_to_message_id"] = message.reply_to.reply_to_msg_id
|
|
292
|
+
|
|
293
|
+
if message.media:
|
|
294
|
+
msg_dict["media_type"] = type(message.media).__name__
|
|
295
|
+
|
|
296
|
+
messages_data.append(msg_dict)
|
|
210
297
|
|
|
211
298
|
return {
|
|
212
299
|
"name": chat_title,
|
|
@@ -250,7 +337,7 @@ async def send_message(
|
|
|
250
337
|
except ImportError:
|
|
251
338
|
raise RuntimeError(
|
|
252
339
|
"Telethon is required. Install with: pip install skmemory[telegram]"
|
|
253
|
-
)
|
|
340
|
+
) from None
|
|
254
341
|
|
|
255
342
|
session_dir = Path(SESSION_PATH).parent
|
|
256
343
|
session_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -266,13 +353,14 @@ async def send_message(
|
|
|
266
353
|
try:
|
|
267
354
|
entity = await client.get_entity(int(chat))
|
|
268
355
|
except (ValueError, TypeError):
|
|
269
|
-
raise RuntimeError(f"Could not find chat: {chat}")
|
|
356
|
+
raise RuntimeError(f"Could not find chat: {chat}") from None
|
|
270
357
|
|
|
271
358
|
# Determine parse mode
|
|
272
359
|
pm = None
|
|
273
360
|
if parse_mode:
|
|
274
361
|
if parse_mode.lower() == "html":
|
|
275
362
|
from telethon.extensions import html as telethon_html # noqa: F401
|
|
363
|
+
|
|
276
364
|
pm = "html"
|
|
277
365
|
elif parse_mode.lower() in ("markdown", "md"):
|
|
278
366
|
pm = "md"
|
|
@@ -322,7 +410,7 @@ async def poll_messages(
|
|
|
322
410
|
except ImportError:
|
|
323
411
|
raise RuntimeError(
|
|
324
412
|
"Telethon is required. Install with: pip install skmemory[telegram]"
|
|
325
|
-
)
|
|
413
|
+
) from None
|
|
326
414
|
|
|
327
415
|
session_dir = Path(SESSION_PATH).parent
|
|
328
416
|
session_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -338,7 +426,7 @@ async def poll_messages(
|
|
|
338
426
|
try:
|
|
339
427
|
entity = await client.get_entity(int(chat))
|
|
340
428
|
except (ValueError, TypeError):
|
|
341
|
-
raise RuntimeError(f"Could not find chat: {chat}")
|
|
429
|
+
raise RuntimeError(f"Could not find chat: {chat}") from None
|
|
342
430
|
|
|
343
431
|
kwargs: dict = {"limit": limit}
|
|
344
432
|
if since:
|
|
@@ -347,7 +435,7 @@ async def poll_messages(
|
|
|
347
435
|
kwargs["offset_date"] = since_dt
|
|
348
436
|
kwargs["reverse"] = True
|
|
349
437
|
except ValueError:
|
|
350
|
-
raise RuntimeError(f"Invalid date format: {since}. Use YYYY-MM-DD.")
|
|
438
|
+
raise RuntimeError(f"Invalid date format: {since}. Use YYYY-MM-DD.") from None
|
|
351
439
|
|
|
352
440
|
messages = []
|
|
353
441
|
async for msg in client.iter_messages(entity, **kwargs):
|
|
@@ -359,15 +447,17 @@ async def poll_messages(
|
|
|
359
447
|
else:
|
|
360
448
|
sender_name = getattr(msg.sender, "title", str(msg.sender_id))
|
|
361
449
|
|
|
362
|
-
messages.append(
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
450
|
+
messages.append(
|
|
451
|
+
{
|
|
452
|
+
"id": msg.id,
|
|
453
|
+
"date": msg.date.isoformat() if msg.date else "",
|
|
454
|
+
"sender": sender_name,
|
|
455
|
+
"sender_id": msg.sender_id,
|
|
456
|
+
"text": msg.text or "",
|
|
457
|
+
"has_media": msg.media is not None,
|
|
458
|
+
"reply_to": msg.reply_to.reply_to_msg_id if msg.reply_to else None,
|
|
459
|
+
}
|
|
460
|
+
)
|
|
371
461
|
|
|
372
462
|
return messages
|
|
373
463
|
finally:
|
|
@@ -397,11 +487,11 @@ async def list_chats(limit: int = 50) -> list[dict]:
|
|
|
397
487
|
|
|
398
488
|
try:
|
|
399
489
|
from telethon import TelegramClient
|
|
400
|
-
from telethon.tl.types import
|
|
490
|
+
from telethon.tl.types import Channel, Chat, User
|
|
401
491
|
except ImportError:
|
|
402
492
|
raise RuntimeError(
|
|
403
493
|
"Telethon is required. Install with: pip install skmemory[telegram]"
|
|
404
|
-
)
|
|
494
|
+
) from None
|
|
405
495
|
|
|
406
496
|
session_dir = Path(SESSION_PATH).parent
|
|
407
497
|
session_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -426,13 +516,15 @@ async def list_chats(limit: int = 50) -> list[dict]:
|
|
|
426
516
|
parts = [entity.first_name or "", entity.last_name or ""]
|
|
427
517
|
title = " ".join(p for p in parts if p) or str(entity.id)
|
|
428
518
|
|
|
429
|
-
chats.append(
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
519
|
+
chats.append(
|
|
520
|
+
{
|
|
521
|
+
"id": entity.id,
|
|
522
|
+
"title": title,
|
|
523
|
+
"type": chat_type,
|
|
524
|
+
"unread_count": dialog.unread_count,
|
|
525
|
+
"username": getattr(entity, "username", None),
|
|
526
|
+
}
|
|
527
|
+
)
|
|
436
528
|
|
|
437
529
|
return chats
|
|
438
530
|
finally:
|
|
@@ -444,11 +536,11 @@ def import_telegram_api(
|
|
|
444
536
|
chat_name_or_id: str,
|
|
445
537
|
*,
|
|
446
538
|
mode: str = "daily",
|
|
447
|
-
limit:
|
|
448
|
-
since:
|
|
539
|
+
limit: int | None = None,
|
|
540
|
+
since: str | None = None,
|
|
449
541
|
min_message_length: int = 30,
|
|
450
|
-
chat_name:
|
|
451
|
-
tags:
|
|
542
|
+
chat_name: str | None = None,
|
|
543
|
+
tags: list[str] | None = None,
|
|
452
544
|
) -> dict:
|
|
453
545
|
"""Import messages directly from Telegram API into SKMemory.
|
|
454
546
|
|
package/skmemory/journal.py
CHANGED
|
@@ -16,10 +16,8 @@ Simple, human-readable, append-only. Like a real journal.
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
-
import os
|
|
20
19
|
from datetime import datetime, timezone
|
|
21
20
|
from pathlib import Path
|
|
22
|
-
from typing import Optional
|
|
23
21
|
|
|
24
22
|
from pydantic import BaseModel, Field
|
|
25
23
|
|
|
@@ -32,9 +30,7 @@ class JournalEntry(BaseModel):
|
|
|
32
30
|
"""A single journal entry for one session."""
|
|
33
31
|
|
|
34
32
|
session_id: str = Field(default="")
|
|
35
|
-
timestamp: str = Field(
|
|
36
|
-
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
37
|
-
)
|
|
33
|
+
timestamp: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
38
34
|
participants: list[str] = Field(
|
|
39
35
|
default_factory=list,
|
|
40
36
|
description="Who was in this session (AI names, human names)",
|
|
@@ -81,12 +77,12 @@ class JournalEntry(BaseModel):
|
|
|
81
77
|
|
|
82
78
|
if self.emotional_summary:
|
|
83
79
|
lines.append("")
|
|
84
|
-
lines.append(
|
|
80
|
+
lines.append("### How It Felt")
|
|
85
81
|
lines.append(self.emotional_summary)
|
|
86
82
|
|
|
87
83
|
if self.notes:
|
|
88
84
|
lines.append("")
|
|
89
|
-
lines.append(
|
|
85
|
+
lines.append("### Notes")
|
|
90
86
|
lines.append(self.notes)
|
|
91
87
|
|
|
92
88
|
lines.append("")
|
package/skmemory/lovenote.py
CHANGED
|
@@ -16,10 +16,8 @@ The notes file lives at ~/.skcapstone/lovenotes.jsonl (JSON Lines).
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
import json
|
|
19
|
-
import os
|
|
20
19
|
from datetime import datetime, timezone
|
|
21
20
|
from pathlib import Path
|
|
22
|
-
from typing import Optional
|
|
23
21
|
|
|
24
22
|
from pydantic import BaseModel, Field
|
|
25
23
|
|
|
@@ -31,9 +29,7 @@ DEFAULT_NOTES_PATH = str(SKMEMORY_HOME / "lovenotes.jsonl")
|
|
|
31
29
|
class LoveNote(BaseModel):
|
|
32
30
|
"""A single heartbeat -- proof of continued connection."""
|
|
33
31
|
|
|
34
|
-
timestamp: str = Field(
|
|
35
|
-
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
36
|
-
)
|
|
32
|
+
timestamp: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
37
33
|
from_name: str = Field(default="", description="Who is sending this note")
|
|
38
34
|
to_name: str = Field(default="", description="Who it's addressed to")
|
|
39
35
|
message: str = Field(
|
|
@@ -111,7 +107,7 @@ class LoveNoteChain:
|
|
|
111
107
|
"""
|
|
112
108
|
if not self.path.exists():
|
|
113
109
|
return 0
|
|
114
|
-
with open(self.path,
|
|
110
|
+
with open(self.path, encoding="utf-8") as f:
|
|
115
111
|
return sum(1 for line in f if line.strip())
|
|
116
112
|
|
|
117
113
|
def read_latest(self, n: int = 10) -> list[LoveNote]:
|
|
@@ -139,7 +135,7 @@ class LoveNoteChain:
|
|
|
139
135
|
return []
|
|
140
136
|
|
|
141
137
|
notes = []
|
|
142
|
-
with open(self.path,
|
|
138
|
+
with open(self.path, encoding="utf-8") as f:
|
|
143
139
|
for line in f:
|
|
144
140
|
line = line.strip()
|
|
145
141
|
if not line:
|
|
@@ -161,10 +157,7 @@ class LoveNoteChain:
|
|
|
161
157
|
list[LoveNote]: Notes from this sender.
|
|
162
158
|
"""
|
|
163
159
|
name_lower = name.lower()
|
|
164
|
-
return [
|
|
165
|
-
n for n in self.read_all()
|
|
166
|
-
if n.from_name.lower() == name_lower
|
|
167
|
-
]
|
|
160
|
+
return [n for n in self.read_all() if n.from_name.lower() == name_lower]
|
|
168
161
|
|
|
169
162
|
def health(self) -> dict:
|
|
170
163
|
"""Check love note chain status.
|