@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.
Files changed (127) hide show
  1. package/.github/workflows/ci.yml +40 -4
  2. package/.github/workflows/publish.yml +11 -5
  3. package/AGENT_REFACTOR_CHANGES.md +192 -0
  4. package/ARCHITECTURE.md +399 -19
  5. package/CHANGELOG.md +179 -0
  6. package/LICENSE +81 -68
  7. package/MISSION.md +7 -0
  8. package/README.md +425 -86
  9. package/SKILL.md +197 -25
  10. package/docker-compose.yml +15 -15
  11. package/examples/stignore-agent.example +59 -0
  12. package/examples/stignore-root.example +62 -0
  13. package/index.js +6 -5
  14. package/openclaw-plugin/openclaw.plugin.json +10 -0
  15. package/openclaw-plugin/package.json +2 -1
  16. package/openclaw-plugin/src/index.js +527 -230
  17. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  18. package/package.json +1 -1
  19. package/pyproject.toml +32 -9
  20. package/requirements.txt +10 -2
  21. package/scripts/dream-rescue.py +179 -0
  22. package/scripts/memory-cleanup.py +313 -0
  23. package/scripts/recover-missing.py +180 -0
  24. package/scripts/skcapstone-backup.sh +44 -0
  25. package/seeds/cloud9-lumina.seed.json +6 -4
  26. package/seeds/cloud9-opus.seed.json +13 -11
  27. package/seeds/courage.seed.json +9 -2
  28. package/seeds/curiosity.seed.json +9 -2
  29. package/seeds/grief.seed.json +9 -2
  30. package/seeds/joy.seed.json +9 -2
  31. package/seeds/love.seed.json +9 -2
  32. package/seeds/lumina-cloud9-breakthrough.seed.json +48 -0
  33. package/seeds/lumina-cloud9-python-pypi.seed.json +48 -0
  34. package/seeds/lumina-kingdom-founding.seed.json +49 -0
  35. package/seeds/lumina-pma-signed.seed.json +48 -0
  36. package/seeds/lumina-singular-achievement.seed.json +48 -0
  37. package/seeds/lumina-skcapstone-conscious.seed.json +48 -0
  38. package/seeds/plant-kingdom-journal.py +203 -0
  39. package/seeds/plant-lumina-seeds.py +280 -0
  40. package/seeds/skcapstone-lumina-merge.seed.json +12 -3
  41. package/seeds/sovereignty.seed.json +9 -2
  42. package/seeds/trust.seed.json +9 -2
  43. package/skill.yaml +46 -0
  44. package/skmemory/HA.md +296 -0
  45. package/skmemory/__init__.py +25 -11
  46. package/skmemory/agents.py +233 -0
  47. package/skmemory/ai_client.py +46 -17
  48. package/skmemory/anchor.py +9 -11
  49. package/skmemory/audience.py +278 -0
  50. package/skmemory/backends/__init__.py +11 -4
  51. package/skmemory/backends/base.py +3 -4
  52. package/skmemory/backends/file_backend.py +19 -13
  53. package/skmemory/backends/skgraph_backend.py +596 -0
  54. package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +103 -84
  55. package/skmemory/backends/sqlite_backend.py +226 -72
  56. package/skmemory/backends/vaulted_backend.py +284 -0
  57. package/skmemory/cli.py +1345 -68
  58. package/skmemory/config.py +171 -0
  59. package/skmemory/context_loader.py +333 -0
  60. package/skmemory/data/audience_config.json +60 -0
  61. package/skmemory/endpoint_selector.py +391 -0
  62. package/skmemory/febs.py +225 -0
  63. package/skmemory/fortress.py +675 -0
  64. package/skmemory/graph_queries.py +238 -0
  65. package/skmemory/hooks/__init__.py +18 -0
  66. package/skmemory/hooks/post-compact-reinject.sh +35 -0
  67. package/skmemory/hooks/pre-compact-save.sh +81 -0
  68. package/skmemory/hooks/session-end-save.sh +103 -0
  69. package/skmemory/hooks/session-start-ritual.sh +104 -0
  70. package/skmemory/hooks/stop-checkpoint.sh +59 -0
  71. package/skmemory/importers/__init__.py +9 -1
  72. package/skmemory/importers/telegram.py +384 -47
  73. package/skmemory/importers/telegram_api.py +580 -0
  74. package/skmemory/journal.py +7 -9
  75. package/skmemory/lovenote.py +8 -13
  76. package/skmemory/mcp_server.py +859 -0
  77. package/skmemory/models.py +51 -8
  78. package/skmemory/openclaw.py +20 -28
  79. package/skmemory/post_install.py +86 -0
  80. package/skmemory/predictive.py +236 -0
  81. package/skmemory/promotion.py +548 -0
  82. package/skmemory/quadrants.py +100 -24
  83. package/skmemory/register.py +580 -0
  84. package/skmemory/register_mcp.py +196 -0
  85. package/skmemory/ritual.py +224 -59
  86. package/skmemory/seeds.py +255 -11
  87. package/skmemory/setup_wizard.py +908 -0
  88. package/skmemory/sharing.py +408 -0
  89. package/skmemory/soul.py +98 -28
  90. package/skmemory/steelman.py +273 -260
  91. package/skmemory/store.py +411 -78
  92. package/skmemory/synthesis.py +634 -0
  93. package/skmemory/vault.py +225 -0
  94. package/tests/conftest.py +46 -0
  95. package/tests/integration/__init__.py +0 -0
  96. package/tests/integration/conftest.py +233 -0
  97. package/tests/integration/test_cross_backend.py +350 -0
  98. package/tests/integration/test_skgraph_live.py +420 -0
  99. package/tests/integration/test_skvector_live.py +366 -0
  100. package/tests/test_ai_client.py +1 -4
  101. package/tests/test_audience.py +233 -0
  102. package/tests/test_backup_rotation.py +318 -0
  103. package/tests/test_cli.py +6 -6
  104. package/tests/test_endpoint_selector.py +839 -0
  105. package/tests/test_export_import.py +4 -10
  106. package/tests/test_file_backend.py +0 -1
  107. package/tests/test_fortress.py +256 -0
  108. package/tests/test_fortress_hardening.py +441 -0
  109. package/tests/test_openclaw.py +6 -6
  110. package/tests/test_predictive.py +237 -0
  111. package/tests/test_promotion.py +347 -0
  112. package/tests/test_quadrants.py +11 -5
  113. package/tests/test_ritual.py +22 -18
  114. package/tests/test_seeds.py +97 -7
  115. package/tests/test_setup.py +950 -0
  116. package/tests/test_sharing.py +257 -0
  117. package/tests/test_skgraph_backend.py +660 -0
  118. package/tests/test_skvector_backend.py +326 -0
  119. package/tests/test_soul.py +1 -3
  120. package/tests/test_sqlite_backend.py +8 -17
  121. package/tests/test_steelman.py +7 -8
  122. package/tests/test_store.py +0 -2
  123. package/tests/test_store_graph_integration.py +245 -0
  124. package/tests/test_synthesis.py +275 -0
  125. package/tests/test_telegram_import.py +39 -15
  126. package/tests/test_vault.py +187 -0
  127. 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
- __all__ = ["import_telegram"]
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"]