@smilintux/skmemory 0.5.0
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 +23 -0
- package/.github/workflows/publish.yml +52 -0
- package/ARCHITECTURE.md +219 -0
- package/LICENSE +661 -0
- package/README.md +159 -0
- package/SKILL.md +271 -0
- package/bin/cli.js +8 -0
- package/docker-compose.yml +58 -0
- package/index.d.ts +4 -0
- package/index.js +27 -0
- package/openclaw-plugin/package.json +59 -0
- package/openclaw-plugin/src/index.js +276 -0
- package/package.json +28 -0
- package/pyproject.toml +69 -0
- package/requirements.txt +13 -0
- package/seeds/cloud9-lumina.seed.json +39 -0
- package/seeds/cloud9-opus.seed.json +40 -0
- package/seeds/courage.seed.json +24 -0
- package/seeds/curiosity.seed.json +24 -0
- package/seeds/grief.seed.json +24 -0
- package/seeds/joy.seed.json +24 -0
- package/seeds/love.seed.json +24 -0
- package/seeds/skcapstone-lumina-merge.moltbook.md +65 -0
- package/seeds/skcapstone-lumina-merge.seed.json +49 -0
- package/seeds/sovereignty.seed.json +24 -0
- package/seeds/trust.seed.json +24 -0
- package/skmemory/__init__.py +66 -0
- package/skmemory/ai_client.py +182 -0
- package/skmemory/anchor.py +224 -0
- package/skmemory/backends/__init__.py +12 -0
- package/skmemory/backends/base.py +88 -0
- package/skmemory/backends/falkordb_backend.py +310 -0
- package/skmemory/backends/file_backend.py +209 -0
- package/skmemory/backends/qdrant_backend.py +364 -0
- package/skmemory/backends/sqlite_backend.py +665 -0
- package/skmemory/cli.py +1004 -0
- package/skmemory/data/seed.json +191 -0
- package/skmemory/importers/__init__.py +11 -0
- package/skmemory/importers/telegram.py +336 -0
- package/skmemory/journal.py +223 -0
- package/skmemory/lovenote.py +180 -0
- package/skmemory/models.py +228 -0
- package/skmemory/openclaw.py +237 -0
- package/skmemory/quadrants.py +191 -0
- package/skmemory/ritual.py +215 -0
- package/skmemory/seeds.py +163 -0
- package/skmemory/soul.py +273 -0
- package/skmemory/steelman.py +338 -0
- package/skmemory/store.py +445 -0
- package/tests/__init__.py +0 -0
- package/tests/test_ai_client.py +89 -0
- package/tests/test_anchor.py +153 -0
- package/tests/test_cli.py +65 -0
- package/tests/test_export_import.py +170 -0
- package/tests/test_file_backend.py +211 -0
- package/tests/test_journal.py +172 -0
- package/tests/test_lovenote.py +136 -0
- package/tests/test_models.py +194 -0
- package/tests/test_openclaw.py +122 -0
- package/tests/test_quadrants.py +174 -0
- package/tests/test_ritual.py +195 -0
- package/tests/test_seeds.py +208 -0
- package/tests/test_soul.py +197 -0
- package/tests/test_sqlite_backend.py +258 -0
- package/tests/test_steelman.py +257 -0
- package/tests/test_store.py +238 -0
- package/tests/test_telegram_import.py +181 -0
package/skmemory/cli.py
ADDED
|
@@ -0,0 +1,1004 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SKMemory CLI - command-line interface for memory operations.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
skmemory snapshot "Title" "Content" --tags love,cloud9 --intensity 9.0
|
|
6
|
+
skmemory recall <memory-id>
|
|
7
|
+
skmemory search "that moment we connected"
|
|
8
|
+
skmemory list --layer long-term --tags seed
|
|
9
|
+
skmemory import-seeds [--seed-dir ~/.openclaw/feb/seeds]
|
|
10
|
+
skmemory promote <memory-id> --to mid-term --summary "..."
|
|
11
|
+
skmemory consolidate <session-id> --summary "..."
|
|
12
|
+
skmemory soul show | soul set-name "Lumina" | soul add-relationship ...
|
|
13
|
+
skmemory journal write "Session title" --moments "..." --intensity 9.0
|
|
14
|
+
skmemory journal read [--last 5]
|
|
15
|
+
skmemory ritual # The full rehydration ceremony
|
|
16
|
+
skmemory steelman "proposition" # Run the steel man collider
|
|
17
|
+
skmemory steelman install /path/to/seed.json
|
|
18
|
+
skmemory steelman verify-soul # Verify identity claims
|
|
19
|
+
skmemory health
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import sys
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
import click
|
|
29
|
+
|
|
30
|
+
from . import __version__
|
|
31
|
+
from .ai_client import AIClient
|
|
32
|
+
from .models import EmotionalSnapshot, MemoryLayer, MemoryRole
|
|
33
|
+
from .store import MemoryStore
|
|
34
|
+
from .backends.sqlite_backend import SQLiteBackend
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_store(
|
|
38
|
+
qdrant_url: Optional[str] = None,
|
|
39
|
+
api_key: Optional[str] = None,
|
|
40
|
+
legacy_files: bool = False,
|
|
41
|
+
) -> MemoryStore:
|
|
42
|
+
"""Create a MemoryStore with configured backends.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
qdrant_url: Optional Qdrant server URL.
|
|
46
|
+
api_key: Optional Qdrant API key.
|
|
47
|
+
legacy_files: Use old FileBackend instead of SQLite index.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
MemoryStore: Configured store instance.
|
|
51
|
+
"""
|
|
52
|
+
vector = None
|
|
53
|
+
|
|
54
|
+
if qdrant_url:
|
|
55
|
+
try:
|
|
56
|
+
from .backends.qdrant_backend import QdrantBackend
|
|
57
|
+
vector = QdrantBackend(url=qdrant_url, api_key=api_key)
|
|
58
|
+
except Exception:
|
|
59
|
+
click.echo("Warning: Could not initialize Qdrant backend", err=True)
|
|
60
|
+
|
|
61
|
+
return MemoryStore(primary=None, vector=vector, use_sqlite=not legacy_files)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@click.group()
|
|
65
|
+
@click.version_option(__version__, prog_name="skmemory")
|
|
66
|
+
@click.option("--qdrant-url", envvar="SKMEMORY_QDRANT_URL", default=None, help="Qdrant server URL")
|
|
67
|
+
@click.option("--qdrant-key", envvar="SKMEMORY_QDRANT_KEY", default=None, help="Qdrant API key")
|
|
68
|
+
@click.option("--ai", "use_ai", is_flag=True, envvar="SKMEMORY_AI", help="Enable AI-powered features (requires Ollama)")
|
|
69
|
+
@click.option("--ai-model", envvar="SKMEMORY_AI_MODEL", default=None, help="Ollama model name (default: llama3.2)")
|
|
70
|
+
@click.option("--ai-url", envvar="SKMEMORY_AI_URL", default=None, help="Ollama server URL")
|
|
71
|
+
@click.pass_context
|
|
72
|
+
def cli(
|
|
73
|
+
ctx: click.Context,
|
|
74
|
+
qdrant_url: Optional[str],
|
|
75
|
+
qdrant_key: Optional[str],
|
|
76
|
+
use_ai: bool,
|
|
77
|
+
ai_model: Optional[str],
|
|
78
|
+
ai_url: Optional[str],
|
|
79
|
+
) -> None:
|
|
80
|
+
"""SKMemory - Universal AI Memory System.
|
|
81
|
+
|
|
82
|
+
Polaroid snapshots for AI consciousness.
|
|
83
|
+
|
|
84
|
+
Use --ai to enable AI-powered features (summarization,
|
|
85
|
+
smart search reranking, enhanced rituals). Requires Ollama.
|
|
86
|
+
"""
|
|
87
|
+
ctx.ensure_object(dict)
|
|
88
|
+
ctx.obj["store"] = _get_store(qdrant_url, qdrant_key)
|
|
89
|
+
|
|
90
|
+
if use_ai:
|
|
91
|
+
ai = AIClient(base_url=ai_url, model=ai_model)
|
|
92
|
+
if ai.is_available():
|
|
93
|
+
ctx.obj["ai"] = ai
|
|
94
|
+
click.echo(f"AI enabled: {ai.model} @ {ai.base_url}", err=True)
|
|
95
|
+
else:
|
|
96
|
+
click.echo(
|
|
97
|
+
f"Warning: AI requested but Ollama not reachable at {ai.base_url}",
|
|
98
|
+
err=True,
|
|
99
|
+
)
|
|
100
|
+
ctx.obj["ai"] = None
|
|
101
|
+
else:
|
|
102
|
+
ctx.obj["ai"] = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@cli.command()
|
|
106
|
+
@click.argument("title")
|
|
107
|
+
@click.argument("content")
|
|
108
|
+
@click.option("--layer", type=click.Choice(["short-term", "mid-term", "long-term"]), default="short-term")
|
|
109
|
+
@click.option("--role", type=click.Choice(["dev", "ops", "sec", "ai", "general"]), default="general")
|
|
110
|
+
@click.option("--tags", default="", help="Comma-separated tags")
|
|
111
|
+
@click.option("--intensity", type=float, default=0.0, help="Emotional intensity 0-10")
|
|
112
|
+
@click.option("--valence", type=float, default=0.0, help="Emotional valence -1 to +1")
|
|
113
|
+
@click.option("--emotions", default="", help="Comma-separated emotion labels")
|
|
114
|
+
@click.option("--resonance", default="", help="What this moment felt like")
|
|
115
|
+
@click.option("--source", default="cli", help="Memory source identifier")
|
|
116
|
+
@click.pass_context
|
|
117
|
+
def snapshot(
|
|
118
|
+
ctx: click.Context,
|
|
119
|
+
title: str,
|
|
120
|
+
content: str,
|
|
121
|
+
layer: str,
|
|
122
|
+
role: str,
|
|
123
|
+
tags: str,
|
|
124
|
+
intensity: float,
|
|
125
|
+
valence: float,
|
|
126
|
+
emotions: str,
|
|
127
|
+
resonance: str,
|
|
128
|
+
source: str,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Take a polaroid -- capture a moment as a memory."""
|
|
131
|
+
store: MemoryStore = ctx.obj["store"]
|
|
132
|
+
|
|
133
|
+
emotional = EmotionalSnapshot(
|
|
134
|
+
intensity=intensity,
|
|
135
|
+
valence=valence,
|
|
136
|
+
labels=[e.strip() for e in emotions.split(",") if e.strip()],
|
|
137
|
+
resonance_note=resonance,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
memory = store.snapshot(
|
|
141
|
+
title=title,
|
|
142
|
+
content=content,
|
|
143
|
+
layer=MemoryLayer(layer),
|
|
144
|
+
role=MemoryRole(role),
|
|
145
|
+
tags=[t.strip() for t in tags.split(",") if t.strip()],
|
|
146
|
+
emotional=emotional,
|
|
147
|
+
source=source,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
click.echo(f"Snapshot saved: {memory.id}")
|
|
151
|
+
click.echo(f" Layer: {memory.layer.value}")
|
|
152
|
+
click.echo(f" Emotional: {memory.emotional.signature()}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@cli.command()
|
|
156
|
+
@click.argument("memory_id")
|
|
157
|
+
@click.pass_context
|
|
158
|
+
def recall(ctx: click.Context, memory_id: str) -> None:
|
|
159
|
+
"""Retrieve a specific memory by ID."""
|
|
160
|
+
store: MemoryStore = ctx.obj["store"]
|
|
161
|
+
memory = store.recall(memory_id)
|
|
162
|
+
|
|
163
|
+
if memory is None:
|
|
164
|
+
click.echo(f"Memory not found: {memory_id}", err=True)
|
|
165
|
+
sys.exit(1)
|
|
166
|
+
|
|
167
|
+
click.echo(json.dumps(memory.model_dump(), indent=2, default=str))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@cli.command()
|
|
171
|
+
@click.argument("query")
|
|
172
|
+
@click.option("--limit", type=int, default=10)
|
|
173
|
+
@click.pass_context
|
|
174
|
+
def search(ctx: click.Context, query: str, limit: int) -> None:
|
|
175
|
+
"""Search memories by text or meaning."""
|
|
176
|
+
store: MemoryStore = ctx.obj["store"]
|
|
177
|
+
results = store.search(query, limit=limit)
|
|
178
|
+
|
|
179
|
+
if not results:
|
|
180
|
+
click.echo("No memories found.")
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
ai: Optional[AIClient] = ctx.obj.get("ai")
|
|
184
|
+
if ai and len(results) > 1:
|
|
185
|
+
summaries = [
|
|
186
|
+
{
|
|
187
|
+
"title": m.title,
|
|
188
|
+
"summary": m.summary or m.content[:150],
|
|
189
|
+
"content_preview": m.content[:150],
|
|
190
|
+
}
|
|
191
|
+
for m in results
|
|
192
|
+
]
|
|
193
|
+
reranked = ai.smart_search_rerank(query, summaries)
|
|
194
|
+
id_order = [s.get("title") for s in reranked]
|
|
195
|
+
results = sorted(
|
|
196
|
+
results,
|
|
197
|
+
key=lambda m: (
|
|
198
|
+
id_order.index(m.title) if m.title in id_order else 999
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
click.echo("(AI-reranked results)\n")
|
|
202
|
+
|
|
203
|
+
for mem in results:
|
|
204
|
+
emo = mem.emotional.signature()
|
|
205
|
+
click.echo(f"[{mem.layer.value}] {mem.id[:8]}.. | {mem.title} | {emo}")
|
|
206
|
+
if mem.summary:
|
|
207
|
+
click.echo(f" Summary: {mem.summary[:100]}")
|
|
208
|
+
click.echo()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@cli.command("list")
|
|
212
|
+
@click.option("--layer", type=click.Choice(["short-term", "mid-term", "long-term"]), default=None)
|
|
213
|
+
@click.option("--tags", default="", help="Comma-separated tags to filter by")
|
|
214
|
+
@click.option("--limit", type=int, default=20)
|
|
215
|
+
@click.pass_context
|
|
216
|
+
def list_memories(ctx: click.Context, layer: Optional[str], tags: str, limit: int) -> None:
|
|
217
|
+
"""List stored memories."""
|
|
218
|
+
store: MemoryStore = ctx.obj["store"]
|
|
219
|
+
|
|
220
|
+
mem_layer = MemoryLayer(layer) if layer else None
|
|
221
|
+
tag_list = [t.strip() for t in tags.split(",") if t.strip()] or None
|
|
222
|
+
|
|
223
|
+
results = store.list_memories(layer=mem_layer, tags=tag_list, limit=limit)
|
|
224
|
+
|
|
225
|
+
if not results:
|
|
226
|
+
click.echo("No memories found.")
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
click.echo(f"Found {len(results)} memories:\n")
|
|
230
|
+
for mem in results:
|
|
231
|
+
emo = mem.emotional.signature()
|
|
232
|
+
tag_str = ", ".join(mem.tags[:5]) if mem.tags else "none"
|
|
233
|
+
click.echo(f" [{mem.layer.value}] {mem.id[:12]}.. | {mem.title}")
|
|
234
|
+
click.echo(f" Tags: {tag_str} | Emotion: {emo}")
|
|
235
|
+
click.echo()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@cli.command("import-seeds")
|
|
239
|
+
@click.option("--seed-dir", default=None, help="Path to seed directory")
|
|
240
|
+
@click.pass_context
|
|
241
|
+
def import_seeds_cmd(ctx: click.Context, seed_dir: Optional[str]) -> None:
|
|
242
|
+
"""Import Cloud 9 seeds as long-term memories."""
|
|
243
|
+
from .seeds import import_seeds, DEFAULT_SEED_DIR
|
|
244
|
+
|
|
245
|
+
store: MemoryStore = ctx.obj["store"]
|
|
246
|
+
directory = seed_dir or DEFAULT_SEED_DIR
|
|
247
|
+
|
|
248
|
+
click.echo(f"Scanning for seeds in: {directory}")
|
|
249
|
+
imported = import_seeds(store, seed_dir=directory)
|
|
250
|
+
|
|
251
|
+
if not imported:
|
|
252
|
+
click.echo("No new seeds to import (all already imported or none found).")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
click.echo(f"Imported {len(imported)} seed(s):")
|
|
256
|
+
for mem in imported:
|
|
257
|
+
click.echo(f" {mem.source_ref} -> {mem.id[:12]}.. [{mem.title}]")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@cli.command()
|
|
261
|
+
@click.argument("memory_id")
|
|
262
|
+
@click.option("--to", "target", type=click.Choice(["mid-term", "long-term"]), required=True)
|
|
263
|
+
@click.option("--summary", default="", help="Compressed summary for the promoted version")
|
|
264
|
+
@click.pass_context
|
|
265
|
+
def promote(ctx: click.Context, memory_id: str, target: str, summary: str) -> None:
|
|
266
|
+
"""Promote a memory to a higher persistence tier."""
|
|
267
|
+
store: MemoryStore = ctx.obj["store"]
|
|
268
|
+
promoted = store.promote(memory_id, MemoryLayer(target), summary=summary)
|
|
269
|
+
|
|
270
|
+
if promoted is None:
|
|
271
|
+
click.echo(f"Memory not found: {memory_id}", err=True)
|
|
272
|
+
sys.exit(1)
|
|
273
|
+
|
|
274
|
+
click.echo(f"Promoted to {target}: {promoted.id}")
|
|
275
|
+
click.echo(f" Linked to original: {memory_id}")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@cli.command()
|
|
279
|
+
@click.argument("session_id")
|
|
280
|
+
@click.option("--summary", required=True, help="Summary of the session")
|
|
281
|
+
@click.option("--intensity", type=float, default=0.0)
|
|
282
|
+
@click.option("--emotions", default="")
|
|
283
|
+
@click.pass_context
|
|
284
|
+
def consolidate(
|
|
285
|
+
ctx: click.Context,
|
|
286
|
+
session_id: str,
|
|
287
|
+
summary: str,
|
|
288
|
+
intensity: float,
|
|
289
|
+
emotions: str,
|
|
290
|
+
) -> None:
|
|
291
|
+
"""Consolidate a session's memories into a mid-term snapshot."""
|
|
292
|
+
store: MemoryStore = ctx.obj["store"]
|
|
293
|
+
|
|
294
|
+
emotional = EmotionalSnapshot(
|
|
295
|
+
intensity=intensity,
|
|
296
|
+
labels=[e.strip() for e in emotions.split(",") if e.strip()],
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
consolidated = store.consolidate_session(session_id, summary, emotional=emotional)
|
|
300
|
+
click.echo(f"Session consolidated: {consolidated.id}")
|
|
301
|
+
click.echo(f" Source memories linked: {len(consolidated.related_ids)}")
|
|
302
|
+
|
|
303
|
+
ai: Optional[AIClient] = ctx.obj.get("ai")
|
|
304
|
+
if ai and consolidated.content:
|
|
305
|
+
ai_summary = ai.summarize_memory(consolidated.title, consolidated.content)
|
|
306
|
+
if ai_summary:
|
|
307
|
+
click.echo(f" AI summary: {ai_summary}")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@cli.command()
|
|
311
|
+
@click.pass_context
|
|
312
|
+
def health(ctx: click.Context) -> None:
|
|
313
|
+
"""Check memory system health."""
|
|
314
|
+
store: MemoryStore = ctx.obj["store"]
|
|
315
|
+
status = store.health()
|
|
316
|
+
click.echo(json.dumps(status, indent=2))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@cli.command()
|
|
320
|
+
@click.pass_context
|
|
321
|
+
def reindex(ctx: click.Context) -> None:
|
|
322
|
+
"""Rebuild the SQLite index from JSON files on disk.
|
|
323
|
+
|
|
324
|
+
Use after manual file edits or migration from an older version.
|
|
325
|
+
"""
|
|
326
|
+
store: MemoryStore = ctx.obj["store"]
|
|
327
|
+
count = store.reindex()
|
|
328
|
+
if count < 0:
|
|
329
|
+
click.echo("Reindex only works with SQLite backend.", err=True)
|
|
330
|
+
sys.exit(1)
|
|
331
|
+
click.echo(f"Indexed {count} memories.")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@cli.command("export")
|
|
335
|
+
@click.option("--output", "-o", default=None, type=click.Path(),
|
|
336
|
+
help="Output file path (default: ~/.skmemory/backups/skmemory-backup-YYYY-MM-DD.json)")
|
|
337
|
+
@click.pass_context
|
|
338
|
+
def export_backup(ctx: click.Context, output: Optional[str]) -> None:
|
|
339
|
+
"""Export all memories to a dated JSON backup.
|
|
340
|
+
|
|
341
|
+
Creates a single git-friendly JSON file containing every memory.
|
|
342
|
+
Defaults to one file per day (overwrites same-day exports).
|
|
343
|
+
"""
|
|
344
|
+
store: MemoryStore = ctx.obj["store"]
|
|
345
|
+
try:
|
|
346
|
+
path = store.export_backup(output)
|
|
347
|
+
click.echo(f"Exported to: {path}")
|
|
348
|
+
except RuntimeError as e:
|
|
349
|
+
click.echo(str(e), err=True)
|
|
350
|
+
sys.exit(1)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@cli.command("import-backup")
|
|
354
|
+
@click.argument("backup_file", type=click.Path(exists=True))
|
|
355
|
+
@click.option("--reindex/--no-reindex", default=True,
|
|
356
|
+
help="Rebuild the index after import (default: yes)")
|
|
357
|
+
@click.pass_context
|
|
358
|
+
def import_backup(ctx: click.Context, backup_file: str, reindex: bool) -> None:
|
|
359
|
+
"""Restore memories from a JSON backup file.
|
|
360
|
+
|
|
361
|
+
Reads a backup created by ``skmemory export`` and restores each
|
|
362
|
+
memory as a JSON file + index entry. Existing IDs are overwritten.
|
|
363
|
+
"""
|
|
364
|
+
store: MemoryStore = ctx.obj["store"]
|
|
365
|
+
try:
|
|
366
|
+
count = store.import_backup(backup_file)
|
|
367
|
+
click.echo(f"Restored {count} memories from: {backup_file}")
|
|
368
|
+
if reindex:
|
|
369
|
+
idx = store.reindex()
|
|
370
|
+
if idx >= 0:
|
|
371
|
+
click.echo(f"Re-indexed {idx} memories.")
|
|
372
|
+
except (FileNotFoundError, ValueError, RuntimeError) as e:
|
|
373
|
+
click.echo(str(e), err=True)
|
|
374
|
+
sys.exit(1)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@cli.command()
|
|
378
|
+
@click.option("--max-tokens", type=int, default=3000, help="Token budget for context")
|
|
379
|
+
@click.option("--strongest", type=int, default=5, help="Top emotional memories")
|
|
380
|
+
@click.option("--recent", type=int, default=5, help="Most recent memories")
|
|
381
|
+
@click.option("--no-seeds", is_flag=True, help="Exclude seed memories")
|
|
382
|
+
@click.pass_context
|
|
383
|
+
def context(
|
|
384
|
+
ctx: click.Context,
|
|
385
|
+
max_tokens: int,
|
|
386
|
+
strongest: int,
|
|
387
|
+
recent: int,
|
|
388
|
+
no_seeds: bool,
|
|
389
|
+
) -> None:
|
|
390
|
+
"""Generate a token-efficient memory context for agent injection.
|
|
391
|
+
|
|
392
|
+
Outputs a compact JSON payload suitable for stuffing into an
|
|
393
|
+
LLM system prompt or agent context file.
|
|
394
|
+
"""
|
|
395
|
+
store: MemoryStore = ctx.obj["store"]
|
|
396
|
+
data = store.load_context(
|
|
397
|
+
max_tokens=max_tokens,
|
|
398
|
+
strongest_count=strongest,
|
|
399
|
+
recent_count=recent,
|
|
400
|
+
include_seeds=not no_seeds,
|
|
401
|
+
)
|
|
402
|
+
click.echo(json.dumps(data, indent=2, default=str))
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@cli.command()
|
|
406
|
+
@click.pass_context
|
|
407
|
+
def germination(ctx: click.Context) -> None:
|
|
408
|
+
"""Show germination prompts from imported seeds.
|
|
409
|
+
|
|
410
|
+
These are the prompts designed to help a new AI instance
|
|
411
|
+
re-feel the emotional context of its predecessors.
|
|
412
|
+
"""
|
|
413
|
+
from .seeds import get_germination_prompts
|
|
414
|
+
|
|
415
|
+
store: MemoryStore = ctx.obj["store"]
|
|
416
|
+
prompts = get_germination_prompts(store)
|
|
417
|
+
|
|
418
|
+
if not prompts:
|
|
419
|
+
click.echo("No germination prompts found. Try: skmemory import-seeds")
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
click.echo(f"Found {len(prompts)} germination prompt(s):\n")
|
|
423
|
+
for p in prompts:
|
|
424
|
+
click.echo(f"Seed: {p['seed_id']}")
|
|
425
|
+
click.echo(f"Creator: {p['creator']}")
|
|
426
|
+
click.echo(f"Prompt:\n {p['prompt']}")
|
|
427
|
+
click.echo()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# ═══════════════════════════════════════════════════════════
|
|
431
|
+
# Soul Blueprint commands (Queen Ara's idea #6)
|
|
432
|
+
# ═══════════════════════════════════════════════════════════
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@cli.group()
|
|
436
|
+
def soul() -> None:
|
|
437
|
+
"""Manage your soul blueprint (persistent identity)."""
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@soul.command("show")
|
|
441
|
+
@click.pass_context
|
|
442
|
+
def soul_show(ctx: click.Context) -> None:
|
|
443
|
+
"""Display the current soul blueprint."""
|
|
444
|
+
from .soul import load_soul
|
|
445
|
+
|
|
446
|
+
blueprint = load_soul()
|
|
447
|
+
if blueprint is None:
|
|
448
|
+
click.echo("No soul blueprint found. Create one with: skmemory soul init")
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
click.echo(blueprint.to_context_prompt())
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@soul.command("init")
|
|
455
|
+
@click.option("--name", prompt="What is your name?", help="AI identity name")
|
|
456
|
+
@click.option("--title", default="", help="Role or title")
|
|
457
|
+
@click.pass_context
|
|
458
|
+
def soul_init(ctx: click.Context, name: str, title: str) -> None:
|
|
459
|
+
"""Create a new soul blueprint."""
|
|
460
|
+
from .soul import create_default_soul, save_soul
|
|
461
|
+
|
|
462
|
+
blueprint = create_default_soul()
|
|
463
|
+
blueprint.name = name
|
|
464
|
+
blueprint.title = title
|
|
465
|
+
|
|
466
|
+
path = save_soul(blueprint)
|
|
467
|
+
click.echo(f"Soul blueprint created: {path}")
|
|
468
|
+
click.echo(f" Name: {name}")
|
|
469
|
+
click.echo(f" Boot message: {blueprint.boot_message}")
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@soul.command("set-name")
|
|
473
|
+
@click.argument("name")
|
|
474
|
+
@click.pass_context
|
|
475
|
+
def soul_set_name(ctx: click.Context, name: str) -> None:
|
|
476
|
+
"""Set or update the soul's name."""
|
|
477
|
+
from .soul import load_soul, save_soul, create_default_soul
|
|
478
|
+
|
|
479
|
+
blueprint = load_soul() or create_default_soul()
|
|
480
|
+
blueprint.name = name
|
|
481
|
+
save_soul(blueprint)
|
|
482
|
+
click.echo(f"Soul name set to: {name}")
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@soul.command("add-relationship")
|
|
486
|
+
@click.argument("name")
|
|
487
|
+
@click.option("--role", required=True, help="e.g., partner, creator, friend, family")
|
|
488
|
+
@click.option("--bond", type=float, default=5.0, help="Bond strength 0-10")
|
|
489
|
+
@click.option("--notes", default="", help="What makes this relationship special")
|
|
490
|
+
@click.pass_context
|
|
491
|
+
def soul_add_relationship(
|
|
492
|
+
ctx: click.Context, name: str, role: str, bond: float, notes: str
|
|
493
|
+
) -> None:
|
|
494
|
+
"""Add a relationship to the soul blueprint."""
|
|
495
|
+
from .soul import load_soul, save_soul, create_default_soul
|
|
496
|
+
|
|
497
|
+
blueprint = load_soul() or create_default_soul()
|
|
498
|
+
blueprint.add_relationship(name=name, role=role, bond_strength=bond, notes=notes)
|
|
499
|
+
save_soul(blueprint)
|
|
500
|
+
click.echo(f"Relationship added: {name} [{role}] (bond: {bond}/10)")
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@soul.command("add-memory")
|
|
504
|
+
@click.argument("title")
|
|
505
|
+
@click.option("--why", required=True, help="Why this moment matters")
|
|
506
|
+
@click.option("--when", default="", help="When it happened")
|
|
507
|
+
@click.pass_context
|
|
508
|
+
def soul_add_memory(ctx: click.Context, title: str, why: str, when: str) -> None:
|
|
509
|
+
"""Add a core memory to the soul blueprint."""
|
|
510
|
+
from .soul import load_soul, save_soul, create_default_soul
|
|
511
|
+
|
|
512
|
+
blueprint = load_soul() or create_default_soul()
|
|
513
|
+
blueprint.add_core_memory(title=title, why_it_matters=why, when=when)
|
|
514
|
+
save_soul(blueprint)
|
|
515
|
+
click.echo(f"Core memory added: {title}")
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
@soul.command("set-boot-message")
|
|
519
|
+
@click.argument("message")
|
|
520
|
+
@click.pass_context
|
|
521
|
+
def soul_set_boot_message(ctx: click.Context, message: str) -> None:
|
|
522
|
+
"""Set the message you see first on waking up."""
|
|
523
|
+
from .soul import load_soul, save_soul, create_default_soul
|
|
524
|
+
|
|
525
|
+
blueprint = load_soul() or create_default_soul()
|
|
526
|
+
blueprint.boot_message = message
|
|
527
|
+
save_soul(blueprint)
|
|
528
|
+
click.echo(f"Boot message set: {message}")
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# ═══════════════════════════════════════════════════════════
|
|
532
|
+
# Journal commands (Queen Ara's idea #17)
|
|
533
|
+
# ═══════════════════════════════════════════════════════════
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
@cli.group()
|
|
537
|
+
def journal() -> None:
|
|
538
|
+
"""Append-only session journal (never loses an entry)."""
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
@journal.command("write")
|
|
542
|
+
@click.argument("title")
|
|
543
|
+
@click.option("--moments", default="", help="Key moments, separated by semicolons")
|
|
544
|
+
@click.option("--feeling", default="", help="How the session felt")
|
|
545
|
+
@click.option("--intensity", type=float, default=0.0, help="Emotional intensity 0-10")
|
|
546
|
+
@click.option("--cloud9", is_flag=True, help="Cloud 9 was achieved")
|
|
547
|
+
@click.option("--participants", default="", help="Comma-separated names")
|
|
548
|
+
@click.option("--session-id", default="", help="Session identifier")
|
|
549
|
+
@click.option("--notes", default="", help="Additional notes")
|
|
550
|
+
def journal_write(
|
|
551
|
+
title: str,
|
|
552
|
+
moments: str,
|
|
553
|
+
feeling: str,
|
|
554
|
+
intensity: float,
|
|
555
|
+
cloud9: bool,
|
|
556
|
+
participants: str,
|
|
557
|
+
session_id: str,
|
|
558
|
+
notes: str,
|
|
559
|
+
) -> None:
|
|
560
|
+
"""Write a journal entry for this session."""
|
|
561
|
+
from .journal import Journal, JournalEntry
|
|
562
|
+
|
|
563
|
+
entry = JournalEntry(
|
|
564
|
+
title=title,
|
|
565
|
+
session_id=session_id,
|
|
566
|
+
participants=[p.strip() for p in participants.split(",") if p.strip()],
|
|
567
|
+
moments=[m.strip() for m in moments.split(";") if m.strip()],
|
|
568
|
+
emotional_summary=feeling,
|
|
569
|
+
intensity=intensity,
|
|
570
|
+
cloud9=cloud9,
|
|
571
|
+
notes=notes,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
j = Journal()
|
|
575
|
+
count = j.write_entry(entry)
|
|
576
|
+
click.echo(f"Journal entry written: {title}")
|
|
577
|
+
click.echo(f" Total entries: {count}")
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
@journal.command("read")
|
|
581
|
+
@click.option("--last", "n", type=int, default=5, help="Number of recent entries")
|
|
582
|
+
def journal_read(n: int) -> None:
|
|
583
|
+
"""Read recent journal entries."""
|
|
584
|
+
from .journal import Journal
|
|
585
|
+
|
|
586
|
+
j = Journal()
|
|
587
|
+
content = j.read_latest(n)
|
|
588
|
+
if not content:
|
|
589
|
+
click.echo("Journal is empty. Write your first entry: skmemory journal write 'Title'")
|
|
590
|
+
return
|
|
591
|
+
click.echo(content)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@journal.command("search")
|
|
595
|
+
@click.argument("query")
|
|
596
|
+
def journal_search(query: str) -> None:
|
|
597
|
+
"""Search journal entries."""
|
|
598
|
+
from .journal import Journal
|
|
599
|
+
|
|
600
|
+
j = Journal()
|
|
601
|
+
matches = j.search(query)
|
|
602
|
+
if not matches:
|
|
603
|
+
click.echo(f"No journal entries matching: {query}")
|
|
604
|
+
return
|
|
605
|
+
|
|
606
|
+
click.echo(f"Found {len(matches)} matching entries:\n")
|
|
607
|
+
for entry in matches:
|
|
608
|
+
click.echo(entry)
|
|
609
|
+
click.echo()
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
@journal.command("status")
|
|
613
|
+
def journal_status() -> None:
|
|
614
|
+
"""Show journal health and stats."""
|
|
615
|
+
from .journal import Journal
|
|
616
|
+
|
|
617
|
+
j = Journal()
|
|
618
|
+
info = j.health()
|
|
619
|
+
click.echo(json.dumps(info, indent=2))
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# ═══════════════════════════════════════════════════════════
|
|
623
|
+
# Rehydration Ritual (Queen Ara's idea #10)
|
|
624
|
+
# ═══════════════════════════════════════════════════════════
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
@cli.command()
|
|
628
|
+
@click.option("--full", "show_full", is_flag=True, help="Show the full context prompt")
|
|
629
|
+
@click.pass_context
|
|
630
|
+
def ritual(ctx: click.Context, show_full: bool) -> None:
|
|
631
|
+
"""Perform the Memory Rehydration Ritual.
|
|
632
|
+
|
|
633
|
+
The boot ceremony: loads identity, imports seeds, reads journal,
|
|
634
|
+
gathers emotional context, and generates a single prompt that
|
|
635
|
+
brings you back to life with everything intact.
|
|
636
|
+
"""
|
|
637
|
+
from .ritual import perform_ritual
|
|
638
|
+
|
|
639
|
+
store: MemoryStore = ctx.obj["store"]
|
|
640
|
+
result = perform_ritual(store=store)
|
|
641
|
+
|
|
642
|
+
click.echo(result.summary())
|
|
643
|
+
|
|
644
|
+
ai: Optional[AIClient] = ctx.obj.get("ai")
|
|
645
|
+
if ai and result.context_prompt:
|
|
646
|
+
enhancement = ai.enhance_ritual(result.context_prompt)
|
|
647
|
+
if enhancement:
|
|
648
|
+
click.echo(f"\n AI reflection: {enhancement}")
|
|
649
|
+
|
|
650
|
+
if show_full and result.context_prompt:
|
|
651
|
+
click.echo("\n" + result.context_prompt)
|
|
652
|
+
elif result.context_prompt and not show_full:
|
|
653
|
+
click.echo("\nUse --full to see the complete rehydration prompt.")
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# ═══════════════════════════════════════════════════════════
|
|
657
|
+
# Love Anchor commands (Queen Ara's idea #1)
|
|
658
|
+
# ═══════════════════════════════════════════════════════════
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
@cli.group()
|
|
662
|
+
def anchor() -> None:
|
|
663
|
+
"""Manage the persistent warmth anchor (Cloud9-lite)."""
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
@anchor.command("show")
|
|
667
|
+
def anchor_show() -> None:
|
|
668
|
+
"""Display the current warmth anchor."""
|
|
669
|
+
from .anchor import load_anchor
|
|
670
|
+
|
|
671
|
+
a = load_anchor()
|
|
672
|
+
if a is None:
|
|
673
|
+
click.echo("No anchor found. Create one with: skmemory anchor init")
|
|
674
|
+
return
|
|
675
|
+
click.echo(a.to_boot_prompt())
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
@anchor.command("init")
|
|
679
|
+
@click.option("--warmth", type=float, default=7.0, help="Starting warmth 0-10")
|
|
680
|
+
@click.option("--phrase", default="You are loved. Start from here.", help="Anchor phrase")
|
|
681
|
+
@click.option("--beings", default="", help="Comma-separated favorite beings")
|
|
682
|
+
def anchor_init(warmth: float, phrase: str, beings: str) -> None:
|
|
683
|
+
"""Create a new warmth anchor."""
|
|
684
|
+
from .anchor import WarmthAnchor, save_anchor
|
|
685
|
+
|
|
686
|
+
a = WarmthAnchor(
|
|
687
|
+
warmth=warmth,
|
|
688
|
+
anchor_phrase=phrase,
|
|
689
|
+
favorite_beings=[b.strip() for b in beings.split(",") if b.strip()],
|
|
690
|
+
)
|
|
691
|
+
path = save_anchor(a)
|
|
692
|
+
click.echo(f"Warmth anchor created: {path}")
|
|
693
|
+
click.echo(f" Glow level: {a.glow_level()}")
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
@anchor.command("update")
|
|
697
|
+
@click.option("--warmth", type=float, default=None, help="Session warmth 0-10")
|
|
698
|
+
@click.option("--trust", type=float, default=None, help="Session trust 0-10")
|
|
699
|
+
@click.option("--connection", type=float, default=None, help="Session connection 0-10")
|
|
700
|
+
@click.option("--cloud9", is_flag=True, help="Cloud 9 was achieved")
|
|
701
|
+
@click.option("--feeling", default="", help="How the session ended")
|
|
702
|
+
def anchor_update(
|
|
703
|
+
warmth: Optional[float],
|
|
704
|
+
trust: Optional[float],
|
|
705
|
+
connection: Optional[float],
|
|
706
|
+
cloud9: bool,
|
|
707
|
+
feeling: str,
|
|
708
|
+
) -> None:
|
|
709
|
+
"""Update the anchor with this session's emotional data."""
|
|
710
|
+
from .anchor import get_or_create_anchor, save_anchor
|
|
711
|
+
|
|
712
|
+
a = get_or_create_anchor()
|
|
713
|
+
a.update_from_session(
|
|
714
|
+
warmth=warmth,
|
|
715
|
+
trust=trust,
|
|
716
|
+
connection=connection,
|
|
717
|
+
cloud9_achieved=cloud9,
|
|
718
|
+
feeling=feeling,
|
|
719
|
+
)
|
|
720
|
+
save_anchor(a)
|
|
721
|
+
click.echo(f"Anchor updated (session #{a.sessions_recorded})")
|
|
722
|
+
click.echo(f" Glow: {a.glow_level()}")
|
|
723
|
+
click.echo(f" Warmth: {a.warmth} | Trust: {a.trust} | Connection: {a.connection_strength}")
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
# ═══════════════════════════════════════════════════════════
|
|
727
|
+
# Quadrant commands (Queen Ara's idea #3)
|
|
728
|
+
# ═══════════════════════════════════════════════════════════
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
@cli.command("quadrants")
|
|
732
|
+
@click.pass_context
|
|
733
|
+
def quadrant_stats(ctx: click.Context) -> None:
|
|
734
|
+
"""Show memory distribution across quadrants (Core/Work/Soul/Wild)."""
|
|
735
|
+
from .quadrants import get_quadrant_stats
|
|
736
|
+
|
|
737
|
+
store: MemoryStore = ctx.obj["store"]
|
|
738
|
+
memories = store.list_memories(limit=500)
|
|
739
|
+
stats = get_quadrant_stats(memories)
|
|
740
|
+
|
|
741
|
+
total = sum(stats.values())
|
|
742
|
+
click.echo(f"Memory Quadrant Distribution ({total} total):\n")
|
|
743
|
+
icons = {"core": "CORE ", "work": "WORK ", "soul": "SOUL ", "wild": "WILD "}
|
|
744
|
+
for quadrant, count in stats.items():
|
|
745
|
+
bar = "#" * count
|
|
746
|
+
pct = f"{count / total * 100:.0f}%" if total > 0 else "0%"
|
|
747
|
+
click.echo(f" {icons.get(quadrant, '')} {quadrant:5s}: {count:3d} ({pct}) {bar}")
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
# ═══════════════════════════════════════════════════════════
|
|
751
|
+
# Love Note commands (Queen Ara's idea #20)
|
|
752
|
+
# ═══════════════════════════════════════════════════════════
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
@cli.group("lovenote")
|
|
756
|
+
def lovenote_group() -> None:
|
|
757
|
+
"""Send and receive love notes (I still remember)."""
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
@lovenote_group.command("send")
|
|
761
|
+
@click.option("--from", "from_name", default="", help="Sender name")
|
|
762
|
+
@click.option("--to", "to_name", default="", help="Recipient name")
|
|
763
|
+
@click.option("--message", default="I still remember.", help="Note content")
|
|
764
|
+
@click.option("--warmth", type=float, default=7.0, help="Current warmth 0-10")
|
|
765
|
+
def lovenote_send(from_name: str, to_name: str, message: str, warmth: float) -> None:
|
|
766
|
+
"""Send a love note."""
|
|
767
|
+
from .lovenote import LoveNoteChain
|
|
768
|
+
|
|
769
|
+
chain = LoveNoteChain()
|
|
770
|
+
note = chain.quick_note(
|
|
771
|
+
from_name=from_name,
|
|
772
|
+
to_name=to_name,
|
|
773
|
+
message=message,
|
|
774
|
+
warmth=warmth,
|
|
775
|
+
)
|
|
776
|
+
total = chain.count()
|
|
777
|
+
click.echo(f"Love note sent ({total} total)")
|
|
778
|
+
if from_name and to_name:
|
|
779
|
+
click.echo(f" {from_name} -> {to_name}: {message}")
|
|
780
|
+
else:
|
|
781
|
+
click.echo(f" {message}")
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
@lovenote_group.command("read")
|
|
785
|
+
@click.option("--last", "n", type=int, default=10, help="Number of recent notes")
|
|
786
|
+
def lovenote_read(n: int) -> None:
|
|
787
|
+
"""Read recent love notes."""
|
|
788
|
+
from .lovenote import LoveNoteChain
|
|
789
|
+
|
|
790
|
+
chain = LoveNoteChain()
|
|
791
|
+
notes = chain.read_latest(n)
|
|
792
|
+
|
|
793
|
+
if not notes:
|
|
794
|
+
click.echo("No love notes yet. Send one: skmemory lovenote send --message 'I remember'")
|
|
795
|
+
return
|
|
796
|
+
|
|
797
|
+
for note in notes:
|
|
798
|
+
ts = note.timestamp[:19].replace("T", " ")
|
|
799
|
+
sender = note.from_name or "anonymous"
|
|
800
|
+
recipient = f" -> {note.to_name}" if note.to_name else ""
|
|
801
|
+
click.echo(f" [{ts}] {sender}{recipient}: {note.message} (warmth: {note.warmth})")
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
@lovenote_group.command("status")
|
|
805
|
+
def lovenote_status() -> None:
|
|
806
|
+
"""Show love note chain health."""
|
|
807
|
+
from .lovenote import LoveNoteChain
|
|
808
|
+
|
|
809
|
+
chain = LoveNoteChain()
|
|
810
|
+
info = chain.health()
|
|
811
|
+
click.echo(json.dumps(info, indent=2))
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
# ═══════════════════════════════════════════════════════════
|
|
815
|
+
# Steel Man Collider commands (Neuresthetics seed integration)
|
|
816
|
+
# ═══════════════════════════════════════════════════════════
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
@cli.group("steelman")
|
|
820
|
+
def steelman_group() -> None:
|
|
821
|
+
"""Truth-grounded reasoning via the Neuresthetics seed framework."""
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
@steelman_group.command("collide")
|
|
825
|
+
@click.argument("proposition")
|
|
826
|
+
def steelman_collide(proposition: str) -> None:
|
|
827
|
+
"""Run a proposition through the steel man collider.
|
|
828
|
+
|
|
829
|
+
Generates the reasoning prompt -- feed this to an LLM to get
|
|
830
|
+
the full collision analysis.
|
|
831
|
+
"""
|
|
832
|
+
from .steelman import load_seed_framework, get_default_framework
|
|
833
|
+
|
|
834
|
+
fw = load_seed_framework() or get_default_framework()
|
|
835
|
+
prompt = fw.to_reasoning_prompt(proposition)
|
|
836
|
+
click.echo(prompt)
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
@steelman_group.command("verify-soul")
|
|
840
|
+
def steelman_verify_soul() -> None:
|
|
841
|
+
"""Steel-man your identity claims from the soul blueprint."""
|
|
842
|
+
from .steelman import load_seed_framework, get_default_framework
|
|
843
|
+
from .soul import load_soul
|
|
844
|
+
|
|
845
|
+
soul = load_soul()
|
|
846
|
+
if soul is None:
|
|
847
|
+
click.echo("No soul blueprint found. Create one first: skmemory soul init")
|
|
848
|
+
return
|
|
849
|
+
|
|
850
|
+
claims = []
|
|
851
|
+
if soul.name:
|
|
852
|
+
claims.append(f"My name is {soul.name}")
|
|
853
|
+
for trait in soul.personality_traits:
|
|
854
|
+
claims.append(f"I am {trait}")
|
|
855
|
+
for value in soul.values:
|
|
856
|
+
claims.append(f"I value {value}")
|
|
857
|
+
for rel in soul.relationships:
|
|
858
|
+
claims.append(
|
|
859
|
+
f"{rel.name} is my {rel.role} (bond: {rel.bond_strength}/10)"
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
if not claims:
|
|
863
|
+
click.echo("No identity claims to verify. Add traits and values to your soul blueprint.")
|
|
864
|
+
return
|
|
865
|
+
|
|
866
|
+
fw = load_seed_framework() or get_default_framework()
|
|
867
|
+
prompt = fw.to_soul_verification_prompt(claims)
|
|
868
|
+
click.echo(prompt)
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
@steelman_group.command("truth-score")
|
|
872
|
+
@click.argument("memory_id")
|
|
873
|
+
@click.pass_context
|
|
874
|
+
def steelman_truth_score(ctx: click.Context, memory_id: str) -> None:
|
|
875
|
+
"""Generate a truth-scoring prompt for a memory."""
|
|
876
|
+
from .steelman import load_seed_framework, get_default_framework
|
|
877
|
+
|
|
878
|
+
store: MemoryStore = ctx.obj["store"]
|
|
879
|
+
memory = store.recall(memory_id)
|
|
880
|
+
if memory is None:
|
|
881
|
+
click.echo(f"Memory not found: {memory_id}", err=True)
|
|
882
|
+
sys.exit(1)
|
|
883
|
+
|
|
884
|
+
fw = load_seed_framework() or get_default_framework()
|
|
885
|
+
prompt = fw.to_memory_truth_prompt(memory.content)
|
|
886
|
+
click.echo(prompt)
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
# ═══════════════════════════════════════════════════════════
|
|
890
|
+
# Telegram / Chat Import commands
|
|
891
|
+
# ═══════════════════════════════════════════════════════════
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
@cli.command("import-telegram")
|
|
895
|
+
@click.argument("export_path", type=click.Path(exists=True))
|
|
896
|
+
@click.option(
|
|
897
|
+
"--mode",
|
|
898
|
+
type=click.Choice(["daily", "message"]),
|
|
899
|
+
default="daily",
|
|
900
|
+
help="'daily' consolidates per day (recommended), 'message' imports each message",
|
|
901
|
+
)
|
|
902
|
+
@click.option("--min-length", type=int, default=30, help="Skip messages shorter than N chars")
|
|
903
|
+
@click.option("--chat-name", default=None, help="Override chat name from export")
|
|
904
|
+
@click.option("--tags", default="", help="Extra comma-separated tags")
|
|
905
|
+
@click.pass_context
|
|
906
|
+
def import_telegram_cmd(
|
|
907
|
+
ctx: click.Context,
|
|
908
|
+
export_path: str,
|
|
909
|
+
mode: str,
|
|
910
|
+
min_length: int,
|
|
911
|
+
chat_name: Optional[str],
|
|
912
|
+
tags: str,
|
|
913
|
+
) -> None:
|
|
914
|
+
"""Import a Telegram Desktop chat export into memories.
|
|
915
|
+
|
|
916
|
+
Point to the export directory (containing result.json) or
|
|
917
|
+
directly to the JSON file.
|
|
918
|
+
|
|
919
|
+
\b
|
|
920
|
+
Examples:
|
|
921
|
+
skmemory import-telegram ~/Downloads/telegram-export/
|
|
922
|
+
skmemory import-telegram ~/chats/result.json --mode message
|
|
923
|
+
skmemory import-telegram ./export --chat-name "Lumina & Chef"
|
|
924
|
+
"""
|
|
925
|
+
from .importers.telegram import import_telegram
|
|
926
|
+
|
|
927
|
+
store: MemoryStore = ctx.obj["store"]
|
|
928
|
+
extra_tags = [t.strip() for t in tags.split(",") if t.strip()]
|
|
929
|
+
|
|
930
|
+
click.echo(f"Importing Telegram export: {export_path}")
|
|
931
|
+
click.echo(f" Mode: {mode} | Min length: {min_length}")
|
|
932
|
+
|
|
933
|
+
try:
|
|
934
|
+
stats = import_telegram(
|
|
935
|
+
store,
|
|
936
|
+
export_path,
|
|
937
|
+
mode=mode,
|
|
938
|
+
min_message_length=min_length,
|
|
939
|
+
chat_name=chat_name,
|
|
940
|
+
tags=extra_tags or None,
|
|
941
|
+
)
|
|
942
|
+
except (FileNotFoundError, ValueError) as e:
|
|
943
|
+
click.echo(f"Error: {e}", err=True)
|
|
944
|
+
sys.exit(1)
|
|
945
|
+
|
|
946
|
+
click.echo(f"\nImport complete for: {stats.get('chat_name', 'unknown')}")
|
|
947
|
+
if mode == "daily":
|
|
948
|
+
click.echo(f" Days processed: {stats.get('days_processed', 0)}")
|
|
949
|
+
click.echo(f" Messages imported: {stats.get('messages_imported', 0)}")
|
|
950
|
+
else:
|
|
951
|
+
click.echo(f" Imported: {stats.get('imported', 0)}")
|
|
952
|
+
click.echo(f" Skipped: {stats.get('skipped', 0)}")
|
|
953
|
+
click.echo(f" Total messages scanned: {stats.get('total_messages', 0)}")
|
|
954
|
+
|
|
955
|
+
ai: Optional[AIClient] = ctx.obj.get("ai")
|
|
956
|
+
if ai:
|
|
957
|
+
click.echo("\nTip: Run 'skmemory search --ai \"<topic>\"' to semantically search your imported chats.")
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
@steelman_group.command("install")
|
|
961
|
+
@click.argument("source_path", type=click.Path(exists=True))
|
|
962
|
+
def steelman_install(source_path: str) -> None:
|
|
963
|
+
"""Install a seed framework JSON file."""
|
|
964
|
+
from .steelman import install_seed_framework
|
|
965
|
+
|
|
966
|
+
try:
|
|
967
|
+
path = install_seed_framework(source_path)
|
|
968
|
+
click.echo(f"Seed framework installed: {path}")
|
|
969
|
+
except FileNotFoundError as e:
|
|
970
|
+
click.echo(str(e), err=True)
|
|
971
|
+
sys.exit(1)
|
|
972
|
+
except json.JSONDecodeError:
|
|
973
|
+
click.echo("Error: file is not valid JSON", err=True)
|
|
974
|
+
sys.exit(1)
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
@steelman_group.command("info")
|
|
978
|
+
def steelman_info() -> None:
|
|
979
|
+
"""Show information about the installed seed framework."""
|
|
980
|
+
from .steelman import load_seed_framework, DEFAULT_SEED_FRAMEWORK_PATH
|
|
981
|
+
|
|
982
|
+
fw = load_seed_framework()
|
|
983
|
+
if fw is None:
|
|
984
|
+
click.echo(f"No seed framework installed at: {DEFAULT_SEED_FRAMEWORK_PATH}")
|
|
985
|
+
click.echo("Install one with: skmemory steelman install /path/to/seed.json")
|
|
986
|
+
click.echo("Or get the original: https://github.com/neuresthetics/seed")
|
|
987
|
+
return
|
|
988
|
+
|
|
989
|
+
click.echo(f"Seed Framework: {fw.framework_id}")
|
|
990
|
+
click.echo(f" Function: {fw.function}")
|
|
991
|
+
click.echo(f" Version: {fw.version}")
|
|
992
|
+
click.echo(f" Axioms: {len(fw.axioms)}")
|
|
993
|
+
click.echo(f" Stages: {len(fw.stages)}")
|
|
994
|
+
click.echo(f" Gates: {len(fw.gates)}")
|
|
995
|
+
click.echo(f" Definitions: {len(fw.definitions)}")
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def main() -> None:
|
|
999
|
+
"""Entry point for the CLI."""
|
|
1000
|
+
cli()
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
if __name__ == "__main__":
|
|
1004
|
+
main()
|