@smilintux/skcapstone 0.6.1 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smilintux/skcapstone",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "SKCapstone - The sovereign agent framework. CapAuth identity, Cloud 9 trust, SKMemory persistence.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -78,6 +78,13 @@ for i in "${!all_files[@]}"; do
78
78
  [ "$old_enough" -eq 1 ] && reason="age=$(( file_age_sec / 3600 ))h"
79
79
  [ "$big_enough" -eq 1 ] && { [ -n "$reason" ] && reason="$reason, "; reason="${reason}size=${file_size_kb}KB"; }
80
80
  log "ARCHIVE ($reason): $basename_f"
81
+ # Save session to skmemory before archiving
82
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
83
+ SESSION_TO_MEM="$SCRIPT_DIR/session-to-memory.py"
84
+ if [ -f "$SESSION_TO_MEM" ]; then
85
+ log " → saving session digest to skmemory..."
86
+ python3 "$SESSION_TO_MEM" "$file" --agent lumina 2>&1 | while IFS= read -r l; do log " $l"; done || true
87
+ fi
81
88
  mv -- "$file" "$ARCHIVE_DIR/$basename_f"
82
89
  archived=$((archived + 1))
83
90
  else
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ session-to-memory.py — Extract an OpenClaw session jsonl and save a digest to skmemory.
4
+
5
+ Usage:
6
+ python3 session-to-memory.py <session.jsonl> [--agent lumina] [--dry-run]
7
+
8
+ Called by archive-sessions.sh before archiving each session file.
9
+ Also useful to run manually against any archived session.
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import os
15
+ import subprocess
16
+ import sys
17
+ import tempfile
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+
21
+ SKIP_PREFIXES = (
22
+ "[SKMemory",
23
+ "[System",
24
+ "[skmemory",
25
+ "--- SKMEMORY",
26
+ "--- SKWHISPER",
27
+ )
28
+
29
+ MAX_CONTENT_CHARS = 12000 # ~3k tokens — enough for a solid digest without blowing budget
30
+ CLAUDE_MODEL = "claude-haiku-4-5" # fast + cheap for digest work
31
+
32
+
33
+ def extract_turns(path: Path) -> list[tuple[str, str]]:
34
+ """Parse a session jsonl and return real (role, text) turns, skipping injections."""
35
+ turns = []
36
+ with open(path, encoding="utf-8", errors="replace") as f:
37
+ for line in f:
38
+ line = line.strip()
39
+ if not line:
40
+ continue
41
+ try:
42
+ obj = json.loads(line)
43
+ except json.JSONDecodeError:
44
+ continue
45
+
46
+ if obj.get("type") != "message":
47
+ continue
48
+
49
+ # Handle both top-level message fields and nested .message
50
+ m = obj.get("message", obj)
51
+ role = m.get("role", "?")
52
+ content = m.get("content", "")
53
+
54
+ if isinstance(content, list):
55
+ text = " ".join(
56
+ c.get("text", "")
57
+ for c in content
58
+ if isinstance(c, dict) and c.get("type") == "text"
59
+ )
60
+ else:
61
+ text = str(content)
62
+
63
+ text = text.strip()
64
+ if not text or len(text) < 5:
65
+ continue
66
+ if any(text.startswith(p) for p in SKIP_PREFIXES):
67
+ continue
68
+
69
+ turns.append((role, text))
70
+
71
+ return turns
72
+
73
+
74
+ def turns_to_prompt(turns: list[tuple[str, str]], max_chars: int = MAX_CONTENT_CHARS) -> str:
75
+ """Format turns as a conversation snippet, truncated to max_chars."""
76
+ lines = []
77
+ for role, text in turns:
78
+ prefix = "Chef" if role == "user" else "Lumina"
79
+ lines.append(f"{prefix}: {text[:600]}")
80
+ full = "\n\n".join(lines)
81
+ if len(full) > max_chars:
82
+ full = full[:max_chars] + "\n\n[... truncated ...]"
83
+ return full
84
+
85
+
86
+ def generate_digest(conversation: str, session_id: str) -> str:
87
+ """Use claude CLI to generate a session digest."""
88
+ prompt = f"""You are summarizing an OpenClaw AI agent session for the skmemory system.
89
+ Session ID: {session_id}
90
+
91
+ Conversation:
92
+ {conversation}
93
+
94
+ Write a concise session digest (3-6 sentences) covering:
95
+ - Key topics discussed
96
+ - Decisions made or actions taken
97
+ - Any notable moments or outcomes
98
+
99
+ Be specific. Use past tense. No preamble."""
100
+
101
+ try:
102
+ result = subprocess.run(
103
+ [
104
+ "claude", "--print",
105
+ "--dangerously-skip-permissions",
106
+ "--model", CLAUDE_MODEL,
107
+ "--output-format", "json",
108
+ "--no-session-persistence",
109
+ ],
110
+ input=prompt.encode(),
111
+ capture_output=True,
112
+ timeout=120,
113
+ )
114
+ if result.returncode != 0:
115
+ return f"[digest failed: {result.stderr.decode()[:200]}]"
116
+ parsed = json.loads(result.stdout.decode())
117
+ return parsed.get("result", "").strip()
118
+ except Exception as e:
119
+ return f"[digest error: {e}]"
120
+
121
+
122
+ def save_to_skmemory(title: str, content: str, agent: str, tags: list[str]) -> bool:
123
+ """Save a memory snapshot via skmemory CLI."""
124
+ tag_str = ",".join(tags)
125
+ try:
126
+ result = subprocess.run(
127
+ [
128
+ "skmemory", "snapshot",
129
+ title, content,
130
+ "--layer", "mid-term",
131
+ "--tags", tag_str,
132
+ ],
133
+ capture_output=True,
134
+ timeout=30,
135
+ env={**os.environ, "SKCAPSTONE_AGENT": agent},
136
+ )
137
+ if result.returncode != 0:
138
+ print(f" [skmemory error] {result.stderr.decode()[:200]}", file=sys.stderr)
139
+ return False
140
+ return True
141
+ except Exception as e:
142
+ print(f" [skmemory exception] {e}", file=sys.stderr)
143
+ return False
144
+
145
+
146
+ def process_session(path: Path, agent: str = "lumina", dry_run: bool = False) -> bool:
147
+ session_id = path.stem[:8]
148
+
149
+ # Infer date from jsonl (first session entry)
150
+ session_date = None
151
+ try:
152
+ with open(path, encoding="utf-8", errors="replace") as f:
153
+ for line in f:
154
+ line = line.strip()
155
+ if not line:
156
+ continue
157
+ obj = json.loads(line)
158
+ if obj.get("type") == "session":
159
+ ts = obj.get("timestamp", "")
160
+ if ts:
161
+ session_date = ts[:10]
162
+ break
163
+ except Exception:
164
+ pass
165
+
166
+ turns = extract_turns(path)
167
+ if not turns:
168
+ print(f" No usable turns in {path.name} — skipping.")
169
+ return False
170
+
171
+ print(f" {len(turns)} turns extracted from {path.name}")
172
+
173
+ date_str = session_date or datetime.now(timezone.utc).strftime("%Y-%m-%d")
174
+ title = f"Session Digest — {date_str} ({session_id})"
175
+
176
+ if dry_run:
177
+ conv = turns_to_prompt(turns)
178
+ print(f" [dry-run] Would save: {title}")
179
+ print(f" Conversation preview ({len(conv)} chars):")
180
+ print(conv[:400])
181
+ return True
182
+
183
+ conversation = turns_to_prompt(turns)
184
+ print(f" Generating digest via claude ({CLAUDE_MODEL})...")
185
+ digest = generate_digest(conversation, session_id)
186
+
187
+ if not digest or digest.startswith("[digest"):
188
+ print(f" Digest generation failed: {digest}")
189
+ return False
190
+
191
+ content = f"**Session:** `{session_id}` \n**Date:** {date_str} \n**Turns:** {len(turns)}\n\n{digest}"
192
+ tags = ["auto-digest", "session-archive", f"session:{session_id}", f"agent:{agent}"]
193
+
194
+ print(f" Saving memory: {title}")
195
+ ok = save_to_skmemory(title, content, agent, tags)
196
+ if ok:
197
+ print(f" Saved to skmemory (mid-term).")
198
+ return ok
199
+
200
+
201
+ def main():
202
+ parser = argparse.ArgumentParser(description="Extract session to skmemory digest")
203
+ parser.add_argument("session_file", help="Path to session .jsonl file")
204
+ parser.add_argument("--agent", default="lumina", help="Agent name (default: lumina)")
205
+ parser.add_argument("--dry-run", action="store_true", help="Preview without saving")
206
+ args = parser.parse_args()
207
+
208
+ path = Path(args.session_file)
209
+ if not path.exists():
210
+ print(f"File not found: {path}", file=sys.stderr)
211
+ sys.exit(1)
212
+
213
+ print(f"Processing: {path.name}")
214
+ ok = process_session(path, agent=args.agent, dry_run=args.dry_run)
215
+ sys.exit(0 if ok else 1)
216
+
217
+
218
+ if __name__ == "__main__":
219
+ main()