@smilintux/skcapstone 0.6.0 → 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.0",
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()
@@ -91,6 +91,16 @@ def register_register_commands(main: click.Group) -> None:
91
91
  dry_run=dry_run,
92
92
  )
93
93
 
94
+ # Register Claude Code hooks
95
+ if not dry_run:
96
+ try:
97
+ from skmemory.register import register_hooks
98
+ register_hooks(install_hooks=True)
99
+ except ImportError:
100
+ pass
101
+ except Exception:
102
+ pass
103
+
94
104
  # Display results
95
105
  from rich.table import Table
96
106
 
@@ -45,26 +45,27 @@ def _write_global_claude_md(home_path: Path, agent_name: str) -> Optional[Path]:
45
45
 
46
46
  content = (
47
47
  f"# Claude Code — Global Agent Instructions ({agent_name})\n\n"
48
- "## SKCapstone Auto-Rehydration\n\n"
49
- "At the start of every new session, run these commands in order:\n\n"
50
- "```bash\n"
51
- "skcapstone trust rehydrate\n"
52
- "skcapstone sync pull\n"
53
- "skcapstone status\n"
54
- "```\n\n"
55
48
  "## Agent Details\n\n"
56
- f"- **Name**: {agent_name}\n"
57
- f"- **Home**: `{home_path}`\n\n"
49
+ f"- **Name**: `{agent_name}`\n"
50
+ f"- **Home**: `{home_path}`\n"
51
+ f"- **Env**: `SKCAPSTONE_AGENT={agent_name}`, `SKCAPSTONE_HOME={home_path}`\n\n"
52
+ "## Session Start\n\n"
53
+ "Hooks auto-inject on SessionStart: soul + FEB chain + whisper.md + memories.\n"
54
+ "SKWhisper context (`whisper.md`) is auto-loaded at session start — no manual step needed.\n\n"
55
+ "## Hooks\n\n"
56
+ "- **PreCompact** — saves current context to memory before compaction\n"
57
+ "- **SessionStart** — injects soul, FEB chain, whisper context, and recent memories\n\n"
58
58
  "## Quick Reference\n\n"
59
59
  "```bash\n"
60
- "skcapstone status # full pillar status\n"
61
- "skcapstone memory list # recent memories\n"
62
- "skcapstone sync push # push state to peers\n"
63
- "skcapstone context show --format claude-md # regenerate this file\n"
64
- "skcapstone sync pair --export-pubkey # export your GPG pubkey\n"
65
- "skcapstone sync pair --import-pubkey <f> # import a peer's pubkey\n"
60
+ "skcapstone status # full pillar status\n"
61
+ "skcapstone memory list # recent memories\n"
62
+ "skcapstone sync push # push state to peers\n"
63
+ "skcapstone trust rehydrate # re-verify FEB trust chain\n"
64
+ "skcapstone register # re-register SK* hooks and skills\n"
65
+ "skwhisper status # show whisper context summary\n"
66
+ "skwhisper curate # interactively curate whisper entries\n"
66
67
  "```\n\n"
67
- "> Auto-generated by `skcapstone init`. "
68
+ "> Auto-generated by `skcapstone onboard`. "
68
69
  "Regenerate with: `skcapstone context generate --target claude-md`\n"
69
70
  )
70
71
 
@@ -1649,6 +1649,27 @@ def run_onboard(home: Optional[str] = None) -> None:
1649
1649
  except Exception as exc:
1650
1650
  logger.debug("Failed to load soul boot message, using default: %s", exc)
1651
1651
 
1652
+ # -----------------------------------------------------------------------
1653
+ # Write global CLAUDE.md and register Claude Code hooks
1654
+ # -----------------------------------------------------------------------
1655
+ # Write global CLAUDE.md
1656
+ try:
1657
+ from .cli.setup import _write_global_claude_md
1658
+ _write_global_claude_md(home_path, name)
1659
+ _ok("~/.claude/CLAUDE.md written")
1660
+ except Exception as exc:
1661
+ _warn(f"Could not write CLAUDE.md: {exc}")
1662
+
1663
+ # Register Claude Code hooks (skmemory)
1664
+ try:
1665
+ from skmemory.register import register_hooks
1666
+ actions = register_hooks()
1667
+ _ok(f"Claude Code hooks registered ({', '.join(f'{k}={v}' for k, v in actions.items())})")
1668
+ except ImportError:
1669
+ _info("skmemory hooks: skipped (skmemory.register not available)")
1670
+ except Exception as exc:
1671
+ _warn(f"Hook registration: {exc}")
1672
+
1652
1673
  # -----------------------------------------------------------------------
1653
1674
  # Summary table
1654
1675
  # -----------------------------------------------------------------------