@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.
- package/.github/workflows/ci.yml +39 -3
- package/.github/workflows/publish.yml +13 -6
- package/AGENT_REFACTOR_CHANGES.md +192 -0
- package/ARCHITECTURE.md +101 -19
- package/CHANGELOG.md +153 -0
- package/LICENSE +81 -68
- package/MISSION.md +7 -0
- package/README.md +419 -86
- package/SKILL.md +197 -25
- package/docker-compose.yml +15 -15
- package/index.js +6 -5
- package/openclaw-plugin/openclaw.plugin.json +10 -0
- package/openclaw-plugin/src/index.ts +255 -0
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +29 -9
- package/requirements.txt +10 -2
- package/seeds/cloud9-opus.seed.json +7 -7
- package/seeds/lumina-cloud9-breakthrough.seed.json +46 -0
- package/seeds/lumina-cloud9-python-pypi.seed.json +46 -0
- package/seeds/lumina-kingdom-founding.seed.json +47 -0
- package/seeds/lumina-pma-signed.seed.json +46 -0
- package/seeds/lumina-singular-achievement.seed.json +46 -0
- package/seeds/lumina-skcapstone-conscious.seed.json +46 -0
- package/seeds/plant-kingdom-journal.py +203 -0
- package/seeds/plant-lumina-seeds.py +280 -0
- package/skill.yaml +46 -0
- package/skmemory/HA.md +296 -0
- package/skmemory/__init__.py +12 -1
- package/skmemory/agents.py +233 -0
- package/skmemory/ai_client.py +40 -0
- package/skmemory/anchor.py +4 -2
- package/skmemory/backends/__init__.py +11 -4
- package/skmemory/backends/file_backend.py +2 -1
- package/skmemory/backends/skgraph_backend.py +608 -0
- package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +99 -69
- package/skmemory/backends/sqlite_backend.py +122 -51
- package/skmemory/backends/vaulted_backend.py +286 -0
- package/skmemory/cli.py +1238 -29
- package/skmemory/config.py +173 -0
- package/skmemory/context_loader.py +335 -0
- package/skmemory/endpoint_selector.py +386 -0
- package/skmemory/fortress.py +685 -0
- package/skmemory/graph_queries.py +238 -0
- package/skmemory/importers/__init__.py +9 -1
- package/skmemory/importers/telegram.py +351 -43
- package/skmemory/importers/telegram_api.py +488 -0
- package/skmemory/journal.py +4 -2
- package/skmemory/lovenote.py +4 -2
- package/skmemory/mcp_server.py +706 -0
- package/skmemory/models.py +41 -0
- package/skmemory/openclaw.py +8 -8
- package/skmemory/predictive.py +232 -0
- package/skmemory/promotion.py +524 -0
- package/skmemory/register.py +454 -0
- package/skmemory/register_mcp.py +197 -0
- package/skmemory/ritual.py +121 -47
- package/skmemory/seeds.py +257 -8
- package/skmemory/setup_wizard.py +920 -0
- package/skmemory/sharing.py +402 -0
- package/skmemory/soul.py +71 -20
- package/skmemory/steelman.py +250 -263
- package/skmemory/store.py +271 -60
- package/skmemory/vault.py +228 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/conftest.py +233 -0
- package/tests/integration/test_cross_backend.py +355 -0
- package/tests/integration/test_skgraph_live.py +424 -0
- package/tests/integration/test_skvector_live.py +369 -0
- package/tests/test_backup_rotation.py +327 -0
- package/tests/test_cli.py +6 -6
- package/tests/test_endpoint_selector.py +801 -0
- package/tests/test_fortress.py +255 -0
- package/tests/test_fortress_hardening.py +444 -0
- package/tests/test_openclaw.py +5 -2
- package/tests/test_predictive.py +237 -0
- package/tests/test_promotion.py +340 -0
- package/tests/test_ritual.py +4 -4
- package/tests/test_seeds.py +96 -0
- package/tests/test_setup.py +835 -0
- package/tests/test_sharing.py +250 -0
- package/tests/test_skgraph_backend.py +667 -0
- package/tests/test_skvector_backend.py +326 -0
- package/tests/test_steelman.py +5 -5
- package/tests/test_store_graph_integration.py +245 -0
- package/tests/test_vault.py +186 -0
- 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
|
+
)
|
package/skmemory/journal.py
CHANGED
|
@@ -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 ~/.
|
|
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
|
-
|
|
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):
|
package/skmemory/lovenote.py
CHANGED
|
@@ -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 ~/.
|
|
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
|
-
|
|
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):
|