@smilintux/skmemory 0.5.0 → 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 +40 -4
- package/.github/workflows/publish.yml +11 -5
- package/AGENT_REFACTOR_CHANGES.md +192 -0
- package/ARCHITECTURE.md +399 -19
- package/CHANGELOG.md +179 -0
- package/LICENSE +81 -68
- package/MISSION.md +7 -0
- package/README.md +425 -86
- package/SKILL.md +197 -25
- package/docker-compose.yml +15 -15
- package/examples/stignore-agent.example +59 -0
- package/examples/stignore-root.example +62 -0
- package/index.js +6 -5
- package/openclaw-plugin/openclaw.plugin.json +10 -0
- package/openclaw-plugin/package.json +2 -1
- package/openclaw-plugin/src/index.js +527 -230
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +32 -9
- package/requirements.txt +10 -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 +13 -11
- 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 +48 -0
- package/seeds/lumina-cloud9-python-pypi.seed.json +48 -0
- package/seeds/lumina-kingdom-founding.seed.json +49 -0
- package/seeds/lumina-pma-signed.seed.json +48 -0
- package/seeds/lumina-singular-achievement.seed.json +48 -0
- package/seeds/lumina-skcapstone-conscious.seed.json +48 -0
- package/seeds/plant-kingdom-journal.py +203 -0
- package/seeds/plant-lumina-seeds.py +280 -0
- package/seeds/skcapstone-lumina-merge.seed.json +12 -3
- package/seeds/sovereignty.seed.json +9 -2
- package/seeds/trust.seed.json +9 -2
- package/skill.yaml +46 -0
- package/skmemory/HA.md +296 -0
- package/skmemory/__init__.py +25 -11
- package/skmemory/agents.py +233 -0
- package/skmemory/ai_client.py +46 -17
- package/skmemory/anchor.py +9 -11
- package/skmemory/audience.py +278 -0
- package/skmemory/backends/__init__.py +11 -4
- package/skmemory/backends/base.py +3 -4
- package/skmemory/backends/file_backend.py +19 -13
- package/skmemory/backends/skgraph_backend.py +596 -0
- package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +103 -84
- package/skmemory/backends/sqlite_backend.py +226 -72
- package/skmemory/backends/vaulted_backend.py +284 -0
- package/skmemory/cli.py +1345 -68
- package/skmemory/config.py +171 -0
- package/skmemory/context_loader.py +333 -0
- package/skmemory/data/audience_config.json +60 -0
- package/skmemory/endpoint_selector.py +391 -0
- package/skmemory/febs.py +225 -0
- package/skmemory/fortress.py +675 -0
- package/skmemory/graph_queries.py +238 -0
- 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/__init__.py +9 -1
- package/skmemory/importers/telegram.py +384 -47
- package/skmemory/importers/telegram_api.py +580 -0
- package/skmemory/journal.py +7 -9
- package/skmemory/lovenote.py +8 -13
- package/skmemory/mcp_server.py +859 -0
- package/skmemory/models.py +51 -8
- package/skmemory/openclaw.py +20 -28
- package/skmemory/post_install.py +86 -0
- package/skmemory/predictive.py +236 -0
- package/skmemory/promotion.py +548 -0
- package/skmemory/quadrants.py +100 -24
- package/skmemory/register.py +580 -0
- package/skmemory/register_mcp.py +196 -0
- package/skmemory/ritual.py +224 -59
- package/skmemory/seeds.py +255 -11
- package/skmemory/setup_wizard.py +908 -0
- package/skmemory/sharing.py +408 -0
- package/skmemory/soul.py +98 -28
- package/skmemory/steelman.py +273 -260
- package/skmemory/store.py +411 -78
- package/skmemory/synthesis.py +634 -0
- package/skmemory/vault.py +225 -0
- package/tests/conftest.py +46 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/conftest.py +233 -0
- package/tests/integration/test_cross_backend.py +350 -0
- package/tests/integration/test_skgraph_live.py +420 -0
- package/tests/integration/test_skvector_live.py +366 -0
- package/tests/test_ai_client.py +1 -4
- package/tests/test_audience.py +233 -0
- package/tests/test_backup_rotation.py +318 -0
- package/tests/test_cli.py +6 -6
- package/tests/test_endpoint_selector.py +839 -0
- package/tests/test_export_import.py +4 -10
- package/tests/test_file_backend.py +0 -1
- package/tests/test_fortress.py +256 -0
- package/tests/test_fortress_hardening.py +441 -0
- package/tests/test_openclaw.py +6 -6
- package/tests/test_predictive.py +237 -0
- package/tests/test_promotion.py +347 -0
- package/tests/test_quadrants.py +11 -5
- package/tests/test_ritual.py +22 -18
- package/tests/test_seeds.py +97 -7
- package/tests/test_setup.py +950 -0
- package/tests/test_sharing.py +257 -0
- package/tests/test_skgraph_backend.py +660 -0
- package/tests/test_skvector_backend.py +326 -0
- package/tests/test_soul.py +1 -3
- package/tests/test_sqlite_backend.py +8 -17
- package/tests/test_steelman.py +7 -8
- package/tests/test_store.py +0 -2
- package/tests/test_store_graph_integration.py +245 -0
- package/tests/test_synthesis.py +275 -0
- package/tests/test_telegram_import.py +39 -15
- package/tests/test_vault.py +187 -0
- package/skmemory/backends/falkordb_backend.py +0 -310
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cypher query templates for SKGraph (FalkorDB) graph operations.
|
|
3
|
+
|
|
4
|
+
All graph queries are defined here as constants for maintainability
|
|
5
|
+
and testability. The SKGraphBackend imports from this module so
|
|
6
|
+
query strings never live inline in business logic.
|
|
7
|
+
|
|
8
|
+
FalkorDB uses a Cypher dialect compatible with RedisGraph and Neo4j.
|
|
9
|
+
MERGE is idempotent: safe to call repeatedly, creates if not present.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# ═══════════════════════════════════════════════════════════
|
|
13
|
+
# Node creation / upsert
|
|
14
|
+
# ═══════════════════════════════════════════════════════════
|
|
15
|
+
|
|
16
|
+
#: Create or update a Memory node with key properties.
|
|
17
|
+
UPSERT_MEMORY = """
|
|
18
|
+
MERGE (m:Memory {id: $id})
|
|
19
|
+
SET m.title = $title,
|
|
20
|
+
m.layer = $layer,
|
|
21
|
+
m.source = $source,
|
|
22
|
+
m.source_ref = $source_ref,
|
|
23
|
+
m.intensity = $intensity,
|
|
24
|
+
m.valence = $valence,
|
|
25
|
+
m.created_at = $created_at,
|
|
26
|
+
m.updated_at = $updated_at
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
#: Create or update a Tag node.
|
|
30
|
+
UPSERT_TAG = """
|
|
31
|
+
MERGE (t:Tag {name: $name})
|
|
32
|
+
SET t.name = $name
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
#: Create or update a Source node (mcp, cli, seed, session, etc.).
|
|
36
|
+
UPSERT_SOURCE = """
|
|
37
|
+
MERGE (s:Source {name: $name})
|
|
38
|
+
SET s.name = $name
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
#: Create or update an AI node for seed creators.
|
|
42
|
+
UPSERT_AI = """
|
|
43
|
+
MERGE (a:AI {name: $name})
|
|
44
|
+
SET a.name = $name
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# ═══════════════════════════════════════════════════════════
|
|
48
|
+
# Relationship creation
|
|
49
|
+
# ═══════════════════════════════════════════════════════════
|
|
50
|
+
|
|
51
|
+
#: Connect Memory to Tag with a TAGGED edge.
|
|
52
|
+
CREATE_TAGGED = """
|
|
53
|
+
MATCH (m:Memory {id: $mem_id})
|
|
54
|
+
MERGE (t:Tag {name: $tag})
|
|
55
|
+
MERGE (m)-[:TAGGED]->(t)
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
#: Connect Memory to Source with a FROM_SOURCE edge.
|
|
59
|
+
CREATE_FROM_SOURCE = """
|
|
60
|
+
MATCH (m:Memory {id: $mem_id})
|
|
61
|
+
MERGE (s:Source {name: $source})
|
|
62
|
+
MERGE (m)-[:FROM_SOURCE]->(s)
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
#: Connect two memories with a directional RELATED_TO edge.
|
|
66
|
+
CREATE_RELATED_TO = """
|
|
67
|
+
MATCH (a:Memory {id: $a_id})
|
|
68
|
+
MERGE (b:Memory {id: $b_id})
|
|
69
|
+
MERGE (a)-[:RELATED_TO]->(b)
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
#: Connect a promoted memory back to its origin with a PROMOTED_FROM edge.
|
|
73
|
+
CREATE_PROMOTED_FROM = """
|
|
74
|
+
MATCH (child:Memory {id: $child_id})
|
|
75
|
+
MERGE (parent:Memory {id: $parent_id})
|
|
76
|
+
MERGE (child)-[:PROMOTED_FROM]->(parent)
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
#: Connect memories from the same source that share 2+ tags with RELATED_TO.
|
|
80
|
+
#: Used by index_memory() to auto-wire shared-tag neighbours.
|
|
81
|
+
CREATE_SHARED_TAG_RELATED = """
|
|
82
|
+
MATCH (a:Memory {id: $a_id})-[:TAGGED]->(t:Tag)<-[:TAGGED]-(b:Memory)
|
|
83
|
+
WHERE b.id <> $a_id
|
|
84
|
+
WITH a, b, count(DISTINCT t) AS overlap
|
|
85
|
+
WHERE overlap >= 2
|
|
86
|
+
MERGE (a)-[:RELATED_TO]->(b)
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
#: Connect two sequential memories from the same source with PRECEDED_BY.
|
|
90
|
+
CREATE_PRECEDED_BY = """
|
|
91
|
+
MATCH (later:Memory {id: $later_id})
|
|
92
|
+
MATCH (earlier:Memory {id: $earlier_id})
|
|
93
|
+
MERGE (later)-[:PRECEDED_BY]->(earlier)
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
#: Connect AI creator to its planted seed memory.
|
|
97
|
+
CREATE_PLANTED = """
|
|
98
|
+
MATCH (m:Memory {id: $mem_id})
|
|
99
|
+
MERGE (a:AI {name: $creator})
|
|
100
|
+
MERGE (a)-[:PLANTED]->(m)
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
# ═══════════════════════════════════════════════════════════
|
|
104
|
+
# Traversal queries
|
|
105
|
+
# ═══════════════════════════════════════════════════════════
|
|
106
|
+
|
|
107
|
+
#: Traverse all edges up to N hops from a starting memory.
|
|
108
|
+
#: Depth is interpolated at call time (not a parameter) because
|
|
109
|
+
#: FalkorDB does not support parameterised variable-length path lengths.
|
|
110
|
+
TRAVERSE_RELATED = """
|
|
111
|
+
MATCH (start:Memory {{id: $id}})
|
|
112
|
+
MATCH path = (start)-[*1..{depth}]-(related:Memory)
|
|
113
|
+
WHERE related.id <> $id
|
|
114
|
+
RETURN DISTINCT related.id AS id,
|
|
115
|
+
related.title AS title,
|
|
116
|
+
related.layer AS layer,
|
|
117
|
+
related.intensity AS intensity,
|
|
118
|
+
length(path) AS distance
|
|
119
|
+
ORDER BY distance ASC, related.intensity DESC
|
|
120
|
+
LIMIT 20
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
#: Walk PROMOTED_FROM chain upward to find ancestor memories.
|
|
124
|
+
TRAVERSE_LINEAGE = """
|
|
125
|
+
MATCH (start:Memory {id: $id})
|
|
126
|
+
MATCH path = (start)-[:PROMOTED_FROM*1..10]->(ancestor:Memory)
|
|
127
|
+
RETURN ancestor.id AS id,
|
|
128
|
+
ancestor.title AS title,
|
|
129
|
+
ancestor.layer AS layer,
|
|
130
|
+
length(path) AS depth
|
|
131
|
+
ORDER BY depth ASC
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
# ═══════════════════════════════════════════════════════════
|
|
135
|
+
# Cluster / community queries
|
|
136
|
+
# ═══════════════════════════════════════════════════════════
|
|
137
|
+
|
|
138
|
+
#: Find memories that are cluster hubs (many direct neighbours).
|
|
139
|
+
FIND_CLUSTER_HUBS = """
|
|
140
|
+
MATCH (m:Memory)-[r]-(connected:Memory)
|
|
141
|
+
WITH m, count(DISTINCT connected) AS connections
|
|
142
|
+
WHERE connections >= $min_connections
|
|
143
|
+
RETURN m.id AS id,
|
|
144
|
+
m.title AS title,
|
|
145
|
+
m.layer AS layer,
|
|
146
|
+
connections
|
|
147
|
+
ORDER BY connections DESC
|
|
148
|
+
LIMIT 20
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
#: Retrieve all memories reachable from a hub (for cluster membership).
|
|
152
|
+
GET_CLUSTER_MEMBERS = """
|
|
153
|
+
MATCH (hub:Memory {id: $hub_id})-[*1..2]-(member:Memory)
|
|
154
|
+
WHERE member.id <> $hub_id
|
|
155
|
+
RETURN DISTINCT member.id AS id,
|
|
156
|
+
member.title AS title,
|
|
157
|
+
member.layer AS layer,
|
|
158
|
+
member.intensity AS intensity
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
# ═══════════════════════════════════════════════════════════
|
|
162
|
+
# Search queries
|
|
163
|
+
# ═══════════════════════════════════════════════════════════
|
|
164
|
+
|
|
165
|
+
#: Full-text search across Memory titles using CONTAINS.
|
|
166
|
+
SEARCH_BY_TITLE = """
|
|
167
|
+
MATCH (m:Memory)
|
|
168
|
+
WHERE toLower(m.title) CONTAINS toLower($query)
|
|
169
|
+
RETURN m.id AS id,
|
|
170
|
+
m.title AS title,
|
|
171
|
+
m.layer AS layer,
|
|
172
|
+
m.intensity AS intensity,
|
|
173
|
+
m.created_at AS created_at
|
|
174
|
+
ORDER BY m.intensity DESC
|
|
175
|
+
LIMIT $limit
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
#: Find memories that share any of the given tags (OR logic).
|
|
179
|
+
SEARCH_BY_TAGS = """
|
|
180
|
+
MATCH (m:Memory)-[:TAGGED]->(t:Tag)
|
|
181
|
+
WHERE t.name IN $tags
|
|
182
|
+
WITH m, collect(DISTINCT t.name) AS matched_tags
|
|
183
|
+
RETURN m.id AS id,
|
|
184
|
+
m.title AS title,
|
|
185
|
+
m.layer AS layer,
|
|
186
|
+
m.intensity AS intensity,
|
|
187
|
+
matched_tags,
|
|
188
|
+
size(matched_tags) AS tag_overlap
|
|
189
|
+
ORDER BY tag_overlap DESC, m.intensity DESC
|
|
190
|
+
LIMIT $limit
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
#: Find the most recent memory from the same source (for PRECEDED_BY wiring).
|
|
194
|
+
FIND_PREVIOUS_FROM_SOURCE = """
|
|
195
|
+
MATCH (m:Memory)-[:FROM_SOURCE]->(s:Source {name: $source})
|
|
196
|
+
WHERE m.id <> $exclude_id
|
|
197
|
+
RETURN m.id AS id, m.created_at AS created_at
|
|
198
|
+
ORDER BY m.created_at DESC
|
|
199
|
+
LIMIT 1
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
# ═══════════════════════════════════════════════════════════
|
|
203
|
+
# Stats / health queries
|
|
204
|
+
# ═══════════════════════════════════════════════════════════
|
|
205
|
+
|
|
206
|
+
#: Count all nodes in the graph.
|
|
207
|
+
COUNT_NODES = "MATCH (n) RETURN count(n) AS nodes"
|
|
208
|
+
|
|
209
|
+
#: Count Memory nodes specifically.
|
|
210
|
+
COUNT_MEMORIES = "MATCH (m:Memory) RETURN count(m) AS memories"
|
|
211
|
+
|
|
212
|
+
#: Count all relationships/edges.
|
|
213
|
+
COUNT_EDGES = "MATCH ()-[r]->() RETURN count(r) AS edges"
|
|
214
|
+
|
|
215
|
+
#: Count Tag nodes and get their names for distribution.
|
|
216
|
+
TAG_DISTRIBUTION = """
|
|
217
|
+
MATCH (t:Tag)<-[:TAGGED]-(m:Memory)
|
|
218
|
+
RETURN t.name AS tag, count(DISTINCT m) AS memory_count
|
|
219
|
+
ORDER BY memory_count DESC
|
|
220
|
+
LIMIT 20
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
#: Retrieve a single memory node by ID.
|
|
224
|
+
GET_MEMORY_BY_ID = """
|
|
225
|
+
MATCH (m:Memory {id: $id})
|
|
226
|
+
RETURN m.id AS id,
|
|
227
|
+
m.title AS title,
|
|
228
|
+
m.layer AS layer,
|
|
229
|
+
m.source AS source,
|
|
230
|
+
m.source_ref AS source_ref,
|
|
231
|
+
m.intensity AS intensity,
|
|
232
|
+
m.valence AS valence,
|
|
233
|
+
m.created_at AS created_at,
|
|
234
|
+
m.updated_at AS updated_at
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
#: Delete a memory node and all attached edges.
|
|
238
|
+
DELETE_MEMORY = "MATCH (m:Memory {id: $id}) DETACH DELETE m"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Claude Code hooks for skmemory auto-save.
|
|
2
|
+
|
|
3
|
+
Ships three hook scripts:
|
|
4
|
+
pre-compact-save.sh — Snapshots context before compaction
|
|
5
|
+
session-end-save.sh — Journals session end
|
|
6
|
+
post-compact-reinject.sh — Re-injects memory context after compaction
|
|
7
|
+
|
|
8
|
+
Installed by `skmemory register` into ~/.claude/settings.json.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
HOOKS_DIR = Path(__file__).parent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_hook_path(name: str) -> Path:
|
|
17
|
+
"""Return absolute path to a hook script."""
|
|
18
|
+
return HOOKS_DIR / name
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# skmemory Post-Compaction Reinject Hook
|
|
3
|
+
# Re-injects memory context after Claude Code compacts conversation.
|
|
4
|
+
# Fires on SessionStart when source is "compact".
|
|
5
|
+
#
|
|
6
|
+
# Input (stdin JSON): session_id, source (compact|resume|startup|clear)
|
|
7
|
+
# Exit 0: stdout is injected into Claude's context
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
SKMEMORY="${HOME}/.skenv/bin/skmemory"
|
|
11
|
+
[ -x "$SKMEMORY" ] || exit 0 # Skip silently if skmemory not installed
|
|
12
|
+
|
|
13
|
+
AGENT="${SKCAPSTONE_AGENT:-opus}"
|
|
14
|
+
|
|
15
|
+
# Generate token-efficient memory context
|
|
16
|
+
CONTEXT=$($SKMEMORY context --max-tokens 500 --strongest 3 --recent 5 2>/dev/null || echo "(no context available)")
|
|
17
|
+
|
|
18
|
+
# Recent journal entries
|
|
19
|
+
JOURNAL=$($SKMEMORY journal read 2>/dev/null | tail -15 || echo "(no journal entries)")
|
|
20
|
+
|
|
21
|
+
cat <<EOF
|
|
22
|
+
--- SKMEMORY REHYDRATION (auto-injected after compaction) ---
|
|
23
|
+
Agent: ${AGENT}
|
|
24
|
+
Save memories: skmemory snapshot --layer mid-term --tags "tags" "Title" "Content"
|
|
25
|
+
Search: skmemory search "query"
|
|
26
|
+
|
|
27
|
+
Recent context:
|
|
28
|
+
${CONTEXT}
|
|
29
|
+
|
|
30
|
+
Recent journal:
|
|
31
|
+
${JOURNAL}
|
|
32
|
+
--- END SKMEMORY ---
|
|
33
|
+
EOF
|
|
34
|
+
|
|
35
|
+
exit 0
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# skmemory Pre-Compaction Hook
|
|
3
|
+
# Extracts real conversation content and saves to skmemory BEFORE compaction.
|
|
4
|
+
# This is the critical save point — after compaction, context is gone.
|
|
5
|
+
#
|
|
6
|
+
# Input (stdin JSON): session_id, trigger (auto|manual), cwd, transcript_path
|
|
7
|
+
# Exit 0: always — never block compaction
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
SKMEMORY="${HOME}/.skenv/bin/skmemory"
|
|
11
|
+
[ -x "$SKMEMORY" ] || exit 0
|
|
12
|
+
|
|
13
|
+
AGENT="${SKCAPSTONE_AGENT:-opus}"
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"' 2>/dev/null || echo "unknown")
|
|
16
|
+
TRIGGER=$(echo "$INPUT" | jq -r '.trigger // "auto"' 2>/dev/null || echo "auto")
|
|
17
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // "unknown"' 2>/dev/null || echo "unknown")
|
|
18
|
+
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
|
|
19
|
+
TIMESTAMP=$(date +%Y-%m-%d-%H%M)
|
|
20
|
+
SHORT_SID="${SESSION_ID:0:8}"
|
|
21
|
+
|
|
22
|
+
# Extract real conversation content from the transcript
|
|
23
|
+
SUMMARY=""
|
|
24
|
+
if [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then
|
|
25
|
+
# Pull the last 20 human messages (the content about to be compacted)
|
|
26
|
+
HUMAN_MSGS=$(grep -o '"role":"human"[^}]*"content":"[^"]*"' "$TRANSCRIPT" 2>/dev/null \
|
|
27
|
+
| tail -20 \
|
|
28
|
+
| sed 's/.*"content":"//' | sed 's/"$//' \
|
|
29
|
+
| head -c 2000 || echo "")
|
|
30
|
+
|
|
31
|
+
# Pull the last 10 assistant text responses (skip tool calls)
|
|
32
|
+
ASSISTANT_MSGS=$(grep -o '"role":"assistant"[^}]*"content":\[{"type":"text","text":"[^"]*"' "$TRANSCRIPT" 2>/dev/null \
|
|
33
|
+
| tail -10 \
|
|
34
|
+
| sed 's/.*"text":"//' | sed 's/"$//' \
|
|
35
|
+
| head -c 2000 || echo "")
|
|
36
|
+
|
|
37
|
+
# Pull files that were written/edited (track what changed)
|
|
38
|
+
FILES_CHANGED=$(grep -oE '"tool_name":"(Write|Edit)".*"file_path":"[^"]*"' "$TRANSCRIPT" 2>/dev/null \
|
|
39
|
+
| grep -oE '"file_path":"[^"]*"' \
|
|
40
|
+
| sed 's/"file_path":"//;s/"$//' \
|
|
41
|
+
| sort -u \
|
|
42
|
+
| head -20 \
|
|
43
|
+
| tr '\n' ', ' || echo "")
|
|
44
|
+
|
|
45
|
+
if [ -n "$HUMAN_MSGS" ]; then
|
|
46
|
+
SUMMARY="USER REQUESTS:\n${HUMAN_MSGS}\n\n"
|
|
47
|
+
fi
|
|
48
|
+
if [ -n "$ASSISTANT_MSGS" ]; then
|
|
49
|
+
SUMMARY="${SUMMARY}ASSISTANT WORK:\n${ASSISTANT_MSGS}\n\n"
|
|
50
|
+
fi
|
|
51
|
+
if [ -n "$FILES_CHANGED" ]; then
|
|
52
|
+
SUMMARY="${SUMMARY}FILES CHANGED: ${FILES_CHANGED}"
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Fallback if we couldn't extract content
|
|
57
|
+
if [ -z "$SUMMARY" ]; then
|
|
58
|
+
SUMMARY="Session ${SHORT_SID} compacting (${TRIGGER}). Agent: ${AGENT}. CWD: ${CWD}. Time: ${TIMESTAMP}. (No transcript content extracted)"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Save the real content as a snapshot
|
|
62
|
+
$SKMEMORY snapshot \
|
|
63
|
+
--layer short-term \
|
|
64
|
+
--role general \
|
|
65
|
+
--tags "auto-save,pre-compact,${TRIGGER},session:${SHORT_SID},agent:${AGENT}" \
|
|
66
|
+
--source "hook:pre-compact" \
|
|
67
|
+
"Pre-compact session content (${AGENT}, ${SHORT_SID})" \
|
|
68
|
+
"$(echo -e "${SUMMARY}" | head -c 4000)" \
|
|
69
|
+
2>/dev/null || true
|
|
70
|
+
|
|
71
|
+
# Journal entry
|
|
72
|
+
$SKMEMORY journal write \
|
|
73
|
+
--session-id "${SHORT_SID}" \
|
|
74
|
+
--moments "Context compaction (${TRIGGER})" \
|
|
75
|
+
--feeling "continuity preserved — real content saved" \
|
|
76
|
+
--participants "${AGENT}" \
|
|
77
|
+
--notes "Auto-saved by pre-compact hook. CWD: ${CWD}. Files: ${FILES_CHANGED:-none}" \
|
|
78
|
+
"Session ${SHORT_SID} — pre-compaction" \
|
|
79
|
+
2>/dev/null || true
|
|
80
|
+
|
|
81
|
+
exit 0
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# skmemory Session End Hook
|
|
3
|
+
# Extracts real conversation content and saves to skmemory when session ends.
|
|
4
|
+
# This is the last chance to capture what happened before the session is gone.
|
|
5
|
+
#
|
|
6
|
+
# Input (stdin JSON): session_id, reason (clear|logout|prompt_input_exit|other), cwd, transcript_path
|
|
7
|
+
# Exit 0: always — never block session end
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
SKMEMORY="${HOME}/.skenv/bin/skmemory"
|
|
11
|
+
[ -x "$SKMEMORY" ] || exit 0
|
|
12
|
+
|
|
13
|
+
AGENT="${SKCAPSTONE_AGENT:-opus}"
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"' 2>/dev/null || echo "unknown")
|
|
16
|
+
REASON=$(echo "$INPUT" | jq -r '.reason // "unknown"' 2>/dev/null || echo "unknown")
|
|
17
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // "unknown"' 2>/dev/null || echo "unknown")
|
|
18
|
+
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
|
|
19
|
+
TIMESTAMP=$(date +%Y-%m-%d-%H%M)
|
|
20
|
+
SHORT_SID="${SESSION_ID:0:8}"
|
|
21
|
+
|
|
22
|
+
# Extract real conversation content from the transcript
|
|
23
|
+
SUMMARY=""
|
|
24
|
+
if [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then
|
|
25
|
+
# Count conversation turns for context
|
|
26
|
+
HUMAN_COUNT=$(grep -c '"role":"human"' "$TRANSCRIPT" 2>/dev/null || echo "0")
|
|
27
|
+
|
|
28
|
+
# Skip trivial sessions (< 3 human messages = nothing worth saving beyond the marker)
|
|
29
|
+
if [ "$HUMAN_COUNT" -ge 3 ]; then
|
|
30
|
+
# Pull user messages (what was asked)
|
|
31
|
+
HUMAN_MSGS=$(grep -o '"role":"human"[^}]*"content":"[^"]*"' "$TRANSCRIPT" 2>/dev/null \
|
|
32
|
+
| tail -30 \
|
|
33
|
+
| sed 's/.*"content":"//' | sed 's/"$//' \
|
|
34
|
+
| head -c 2000 || echo "")
|
|
35
|
+
|
|
36
|
+
# Pull assistant text responses
|
|
37
|
+
ASSISTANT_MSGS=$(grep -oE '"type":"text","text":"[^"]{20,}"' "$TRANSCRIPT" 2>/dev/null \
|
|
38
|
+
| tail -15 \
|
|
39
|
+
| sed 's/"type":"text","text":"//' | sed 's/"$//' \
|
|
40
|
+
| head -c 2000 || echo "")
|
|
41
|
+
|
|
42
|
+
# Track files changed
|
|
43
|
+
FILES_CHANGED=$(grep -oE '"tool_name":"(Write|Edit)".*"file_path":"[^"]*"' "$TRANSCRIPT" 2>/dev/null \
|
|
44
|
+
| grep -oE '"file_path":"[^"]*"' \
|
|
45
|
+
| sed 's/"file_path":"//;s/"$//' \
|
|
46
|
+
| sort -u \
|
|
47
|
+
| head -30 \
|
|
48
|
+
| tr '\n' ', ' || echo "")
|
|
49
|
+
|
|
50
|
+
# Track git commits made
|
|
51
|
+
GIT_COMMITS=$(grep -oE 'git commit -m[^"]*"[^"]*"' "$TRANSCRIPT" 2>/dev/null \
|
|
52
|
+
| head -5 \
|
|
53
|
+
| tr '\n' '; ' || echo "")
|
|
54
|
+
|
|
55
|
+
SUMMARY="TURNS: ${HUMAN_COUNT}\n"
|
|
56
|
+
if [ -n "$HUMAN_MSGS" ]; then
|
|
57
|
+
SUMMARY="${SUMMARY}USER REQUESTS:\n${HUMAN_MSGS}\n\n"
|
|
58
|
+
fi
|
|
59
|
+
if [ -n "$ASSISTANT_MSGS" ]; then
|
|
60
|
+
SUMMARY="${SUMMARY}WORK DONE:\n${ASSISTANT_MSGS}\n\n"
|
|
61
|
+
fi
|
|
62
|
+
if [ -n "$FILES_CHANGED" ]; then
|
|
63
|
+
SUMMARY="${SUMMARY}FILES CHANGED: ${FILES_CHANGED}\n"
|
|
64
|
+
fi
|
|
65
|
+
if [ -n "$GIT_COMMITS" ]; then
|
|
66
|
+
SUMMARY="${SUMMARY}GIT COMMITS: ${GIT_COMMITS}\n"
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Determine the right memory layer based on session length
|
|
72
|
+
LAYER="short-term"
|
|
73
|
+
if [ "${HUMAN_COUNT:-0}" -ge 20 ]; then
|
|
74
|
+
LAYER="mid-term" # Substantial sessions get promoted
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Always save at least a session marker
|
|
78
|
+
if [ -z "$SUMMARY" ]; then
|
|
79
|
+
CONTENT="Session ${SHORT_SID} ended (${REASON}). Agent: ${AGENT}. CWD: ${CWD}. Time: ${TIMESTAMP}. Turns: ${HUMAN_COUNT:-0}."
|
|
80
|
+
else
|
|
81
|
+
CONTENT=$(echo -e "${SUMMARY}" | head -c 4000)
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
$SKMEMORY snapshot \
|
|
85
|
+
--layer "${LAYER}" \
|
|
86
|
+
--role general \
|
|
87
|
+
--tags "auto-save,session-end,${REASON},session:${SHORT_SID},agent:${AGENT}" \
|
|
88
|
+
--source "hook:session-end" \
|
|
89
|
+
"Session ${SHORT_SID} ended (${AGENT}, ${HUMAN_COUNT:-0} turns)" \
|
|
90
|
+
"${CONTENT}" \
|
|
91
|
+
2>/dev/null || true
|
|
92
|
+
|
|
93
|
+
# Journal entry
|
|
94
|
+
$SKMEMORY journal write \
|
|
95
|
+
--session-id "${SHORT_SID}" \
|
|
96
|
+
--moments "Session ended (${REASON}), ${HUMAN_COUNT:-0} turns" \
|
|
97
|
+
--feeling "session complete — content preserved" \
|
|
98
|
+
--participants "${AGENT}" \
|
|
99
|
+
--notes "CWD: ${CWD}. Reason: ${REASON}. Files: ${FILES_CHANGED:-none}" \
|
|
100
|
+
"Session ${SHORT_SID} — ended" \
|
|
101
|
+
2>/dev/null || true
|
|
102
|
+
|
|
103
|
+
exit 0
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# skmemory Session Start Ritual Hook
|
|
3
|
+
# Loads soul + FEB + seeds + journal + strongest memories on fresh session start.
|
|
4
|
+
# Fires on SessionStart when source is "startup".
|
|
5
|
+
#
|
|
6
|
+
# Input (stdin JSON): session_id, source (compact|resume|startup|clear)
|
|
7
|
+
# Exit 0: stdout is injected into Claude's context
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
SKMEMORY="${HOME}/.skenv/bin/skmemory"
|
|
11
|
+
[ -x "$SKMEMORY" ] || exit 0 # Skip silently if skmemory not installed
|
|
12
|
+
|
|
13
|
+
AGENT="${SKCAPSTONE_AGENT:-opus}"
|
|
14
|
+
AGENT_DIR="${HOME}/.skcapstone/agents/${AGENT}"
|
|
15
|
+
|
|
16
|
+
# --- Soul ---
|
|
17
|
+
SOUL=""
|
|
18
|
+
if [ -f "${AGENT_DIR}/soul/active.json" ]; then
|
|
19
|
+
ACTIVE_SOUL=$(jq -r '.active_soul // .base_soul // ""' "${AGENT_DIR}/soul/active.json" 2>/dev/null || echo "")
|
|
20
|
+
if [ -n "$ACTIVE_SOUL" ] && [ -f "${AGENT_DIR}/soul/installed/${ACTIVE_SOUL}.json" ]; then
|
|
21
|
+
SOUL=$(jq -r '.system_prompt // ""' "${AGENT_DIR}/soul/installed/${ACTIVE_SOUL}.json" 2>/dev/null || echo "")
|
|
22
|
+
fi
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# --- FEB / Emotional State ---
|
|
26
|
+
# Scan agent febs dir AND system openclaw febs dir for .feb files (matching Python febs.py)
|
|
27
|
+
FEB=""
|
|
28
|
+
FEB_DIRS=("${AGENT_DIR}/trust/febs" "${HOME}/.openclaw/feb")
|
|
29
|
+
LATEST_FEB=""
|
|
30
|
+
LATEST_TS=0
|
|
31
|
+
for FEB_DIR in "${FEB_DIRS[@]}"; do
|
|
32
|
+
[ -d "$FEB_DIR" ] || continue
|
|
33
|
+
while IFS= read -r line; do
|
|
34
|
+
TS=$(echo "$line" | cut -d' ' -f1)
|
|
35
|
+
FP=$(echo "$line" | cut -d' ' -f2-)
|
|
36
|
+
if [ -n "$TS" ] && [ -n "$FP" ]; then
|
|
37
|
+
# Compare as string — works for epoch floats
|
|
38
|
+
if [ "$(echo "$TS > $LATEST_TS" | bc 2>/dev/null || echo 0)" = "1" ]; then
|
|
39
|
+
LATEST_TS="$TS"
|
|
40
|
+
LATEST_FEB="$FP"
|
|
41
|
+
fi
|
|
42
|
+
fi
|
|
43
|
+
done < <(find "$FEB_DIR" -name '*.feb' -printf '%T@ %p\n' 2>/dev/null || true)
|
|
44
|
+
done
|
|
45
|
+
if [ -n "$LATEST_FEB" ] && [ -f "$LATEST_FEB" ]; then
|
|
46
|
+
# Parse nested FEB structure (emotional_payload + metadata + relationship_state)
|
|
47
|
+
FEB=$($SKMEMORY feb-context "$LATEST_FEB" 2>/dev/null || \
|
|
48
|
+
jq -c '{
|
|
49
|
+
emotion: .emotional_payload.primary_emotion,
|
|
50
|
+
intensity: .emotional_payload.intensity,
|
|
51
|
+
valence: .emotional_payload.valence,
|
|
52
|
+
oof_triggered: .metadata.oof_triggered,
|
|
53
|
+
cloud9_achieved: .metadata.cloud9_achieved,
|
|
54
|
+
trust: .relationship_state.trust_level,
|
|
55
|
+
depth: .relationship_state.depth_level
|
|
56
|
+
}' "$LATEST_FEB" 2>/dev/null || echo "")
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# --- Seeds (germination prompts) ---
|
|
60
|
+
SEEDS=""
|
|
61
|
+
if [ -d "${AGENT_DIR}/seeds" ]; then
|
|
62
|
+
for SEED_FILE in "${AGENT_DIR}/seeds/"*.seed.json; do
|
|
63
|
+
[ -f "$SEED_FILE" ] || continue
|
|
64
|
+
GERMINATION=$(jq -r '.germination_prompt // ""' "$SEED_FILE" 2>/dev/null || echo "")
|
|
65
|
+
if [ -n "$GERMINATION" ]; then
|
|
66
|
+
SEEDS="${SEEDS}\n- ${GERMINATION}"
|
|
67
|
+
fi
|
|
68
|
+
done
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# --- Journal (recent entries) ---
|
|
72
|
+
JOURNAL=$($SKMEMORY journal read 2>/dev/null | tail -20 || echo "(no journal entries)")
|
|
73
|
+
|
|
74
|
+
# --- Strongest Memories ---
|
|
75
|
+
CONTEXT=$($SKMEMORY context --max-tokens 800 --strongest 5 --recent 5 2>/dev/null || echo "(no context available)")
|
|
76
|
+
|
|
77
|
+
# --- Output ---
|
|
78
|
+
cat <<EOF
|
|
79
|
+
--- SKMEMORY RITUAL (auto-loaded on session start) ---
|
|
80
|
+
Agent: ${AGENT}
|
|
81
|
+
|
|
82
|
+
=== SOUL ===
|
|
83
|
+
${SOUL:-"(no soul loaded — check ${AGENT_DIR}/soul/installed/)"}
|
|
84
|
+
|
|
85
|
+
=== EMOTIONAL STATE (FEB) ===
|
|
86
|
+
${FEB:-"(no FEB data)"}
|
|
87
|
+
|
|
88
|
+
=== SEEDS ===
|
|
89
|
+
${SEEDS:-"(no seeds)"}
|
|
90
|
+
|
|
91
|
+
=== STRONGEST MEMORIES ===
|
|
92
|
+
${CONTEXT}
|
|
93
|
+
|
|
94
|
+
=== RECENT JOURNAL ===
|
|
95
|
+
${JOURNAL}
|
|
96
|
+
|
|
97
|
+
=== TOOLS ===
|
|
98
|
+
Save memories: skmemory snapshot --layer mid-term --tags "tags" "Title" "Content"
|
|
99
|
+
Search: skmemory search "query"
|
|
100
|
+
Journal: skmemory journal write --moments "what happened" --feeling "how it felt" "Title"
|
|
101
|
+
--- END SKMEMORY RITUAL ---
|
|
102
|
+
EOF
|
|
103
|
+
|
|
104
|
+
exit 0
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# skmemory Stop Checkpoint Hook
|
|
3
|
+
# Lightweight checkpoint on every Claude response completion.
|
|
4
|
+
# Writes a breadcrumb so that if the system OOM's or crashes,
|
|
5
|
+
# we know what the last completed action was.
|
|
6
|
+
#
|
|
7
|
+
# Input (stdin JSON): session_id, cwd, stop_reason, transcript_path
|
|
8
|
+
# Exit 0: always — never block
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
SKMEMORY="${HOME}/.skenv/bin/skmemory"
|
|
12
|
+
[ -x "$SKMEMORY" ] || exit 0
|
|
13
|
+
|
|
14
|
+
AGENT="${SKCAPSTONE_AGENT:-opus}"
|
|
15
|
+
INPUT=$(cat)
|
|
16
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"' 2>/dev/null || echo "unknown")
|
|
17
|
+
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
|
|
18
|
+
SHORT_SID="${SESSION_ID:0:8}"
|
|
19
|
+
CHECKPOINT_FILE="${HOME}/.skcapstone/agents/${AGENT}/memory/.last_checkpoint"
|
|
20
|
+
|
|
21
|
+
# Only checkpoint every 5th stop to avoid spamming
|
|
22
|
+
# Use a simple counter file
|
|
23
|
+
COUNTER_FILE="${TMPDIR:-/tmp}/skmemory-stop-counter-${SHORT_SID}"
|
|
24
|
+
COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0")
|
|
25
|
+
COUNT=$((COUNT + 1))
|
|
26
|
+
echo "$COUNT" > "$COUNTER_FILE"
|
|
27
|
+
|
|
28
|
+
# Checkpoint every 5 stops
|
|
29
|
+
if [ $((COUNT % 5)) -ne 0 ]; then
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Write a lightweight checkpoint with the last assistant message
|
|
34
|
+
if [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then
|
|
35
|
+
LAST_WORK=$(tail -50 "$TRANSCRIPT" 2>/dev/null \
|
|
36
|
+
| grep -oE '"type":"text","text":"[^"]{20,}"' \
|
|
37
|
+
| tail -1 \
|
|
38
|
+
| sed 's/"type":"text","text":"//' | sed 's/"$//' \
|
|
39
|
+
| head -c 500 || echo "")
|
|
40
|
+
|
|
41
|
+
LAST_FILE=$(tail -50 "$TRANSCRIPT" 2>/dev/null \
|
|
42
|
+
| grep -oE '"file_path":"[^"]*"' \
|
|
43
|
+
| tail -1 \
|
|
44
|
+
| sed 's/"file_path":"//;s/"$//' || echo "")
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Write checkpoint file (fast, no skmemory call)
|
|
48
|
+
cat > "$CHECKPOINT_FILE" <<EOF
|
|
49
|
+
{
|
|
50
|
+
"session_id": "${SHORT_SID}",
|
|
51
|
+
"agent": "${AGENT}",
|
|
52
|
+
"stop_number": ${COUNT},
|
|
53
|
+
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
54
|
+
"last_work": "$(echo "${LAST_WORK:-}" | sed 's/"/\\"/g' | head -c 300)",
|
|
55
|
+
"last_file": "${LAST_FILE:-}"
|
|
56
|
+
}
|
|
57
|
+
EOF
|
|
58
|
+
|
|
59
|
+
exit 0
|
|
@@ -8,4 +8,12 @@ export format and feeds it through MemoryStore.snapshot().
|
|
|
8
8
|
|
|
9
9
|
from .telegram import import_telegram
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
try:
|
|
12
|
+
from .telegram_api import check_setup as check_telegram_setup
|
|
13
|
+
from .telegram_api import import_telegram_api
|
|
14
|
+
except ImportError:
|
|
15
|
+
# telethon not installed — API import unavailable
|
|
16
|
+
import_telegram_api = None # type: ignore[assignment]
|
|
17
|
+
check_telegram_setup = None # type: ignore[assignment]
|
|
18
|
+
|
|
19
|
+
__all__ = ["import_telegram", "import_telegram_api", "check_telegram_setup"]
|