@smilintux/skcapstone 0.6.1 → 0.6.3
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/publish.yml +1 -1
- package/CLAUDE.md +17 -0
- package/docs/CUSTOM_AGENT.md +40 -28
- package/docs/SOUL_SWAPPER.md +5 -5
- package/docs/hammertime-audit.md +402 -0
- package/openclaw-plugin/src/index.ts +2 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/archive-sessions.sh +7 -0
- package/scripts/install.sh +126 -1
- package/scripts/model-fallback-monitor.sh +4 -2
- package/scripts/refresh-anthropic-token.sh +9 -3
- package/scripts/release.sh +98 -0
- package/scripts/session-to-memory.py +219 -0
- package/scripts/sk-agent-picker.sh +237 -0
- package/scripts/telegram-catchup-all.sh +2 -1
- package/scripts/watch-anthropic-token.sh +12 -17
- package/src/skcapstone/__init__.py +34 -2
- package/src/skcapstone/cli/__init__.py +3 -1
- package/src/skcapstone/cli/_common.py +1 -0
- package/src/skcapstone/cli/context_cmd.py +16 -4
- package/src/skcapstone/cli/daemon.py +2 -1
- package/src/skcapstone/cli/joule_cmd.py +7 -3
- package/src/skcapstone/cli/memory.py +4 -2
- package/src/skcapstone/cli/register_cmd.py +19 -3
- package/src/skcapstone/cli/session.py +25 -0
- package/src/skcapstone/cli/setup.py +96 -30
- package/src/skcapstone/cli/soul.py +3 -3
- package/src/skcapstone/context_loader.py +9 -0
- package/src/skcapstone/coordination.py +9 -2
- package/src/skcapstone/daemon.py +22 -12
- package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
- package/src/skcapstone/defaults/claude/settings.json +74 -0
- package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
- package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
- package/src/skcapstone/defaults/unhinged.json +13 -0
- package/src/skcapstone/discovery.py +5 -5
- package/src/skcapstone/doctor.py +4 -2
- package/src/skcapstone/dreaming.py +3 -1
- package/src/skcapstone/fuse_mount.py +3 -1
- package/src/skcapstone/housekeeping.py +3 -3
- package/src/skcapstone/install_wizard.py +131 -0
- package/src/skcapstone/mcp_launcher.py +14 -1
- package/src/skcapstone/mcp_server.py +6 -21
- package/src/skcapstone/mcp_tools/notification_tools.py +3 -1
- package/src/skcapstone/memory_engine.py +10 -3
- package/src/skcapstone/migrate_multi_agent.py +7 -6
- package/src/skcapstone/notifications.py +6 -2
- package/src/skcapstone/onboard.py +19 -8
- package/src/skcapstone/operator_link.py +164 -0
- package/src/skcapstone/pillars/consciousness.py +2 -1
- package/src/skcapstone/pillars/identity.py +51 -7
- package/src/skcapstone/pillars/memory.py +9 -3
- package/src/skcapstone/runtime.py +13 -3
- package/src/skcapstone/service_health.py +23 -10
- package/src/skcapstone/session_briefing.py +108 -0
- package/src/skcapstone/trust_graph.py +40 -5
- package/src/skcapstone/unified_search.py +11 -2
- package/systemd/skcapstone.service +4 -6
- package/systemd/skcapstone@.service +7 -8
- package/systemd/skcomm-heartbeat.service +5 -2
- package/tests/conftest.py +21 -0
- package/tests/test_agent_home_scaffold.py +34 -0
- package/tests/test_backup.py +2 -1
- package/tests/test_mcp_server.py +78 -33
- package/tests/test_multi_agent.py +31 -29
- package/tests/test_operator_link.py +78 -0
- package/tests/test_runtime.py +21 -0
- package/tests/test_session_briefing.py +130 -0
- package/tests/test_trust_graph.py +18 -0
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "skcapstone"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.3"
|
|
8
8
|
description = "Sovereign Agent Framework — conscious AI through identity, trust, memory, and security"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "GPL-3.0-or-later"}
|
|
@@ -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
|
package/scripts/install.sh
CHANGED
|
@@ -176,6 +176,46 @@ else
|
|
|
176
176
|
done
|
|
177
177
|
fi
|
|
178
178
|
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Install sk-agent-picker.sh to ~/.skenv/share/skcapstone/ and wire bashrc
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
PICKER_SRC="$REPO_ROOT/scripts/sk-agent-picker.sh"
|
|
183
|
+
SHARE_DIR="$SKENV/share/skcapstone"
|
|
184
|
+
PICKER_DEST="$SHARE_DIR/sk-agent-picker.sh"
|
|
185
|
+
|
|
186
|
+
if [[ -f "$PICKER_SRC" ]]; then
|
|
187
|
+
mkdir -p "$SHARE_DIR"
|
|
188
|
+
cp "$PICKER_SRC" "$PICKER_DEST"
|
|
189
|
+
chmod +x "$PICKER_DEST"
|
|
190
|
+
echo " sk-agent-picker installed → $PICKER_DEST"
|
|
191
|
+
|
|
192
|
+
# Wire into shell rc files — replaces a bare claude alias if present
|
|
193
|
+
_PICKER_SNIPPET=$(cat <<'SNIPPET'
|
|
194
|
+
|
|
195
|
+
# SKCapstone agent picker + skswitch — prompts for agent when multiple are found.
|
|
196
|
+
# Sourced by install.sh; honours pre-set SKAGENT without prompting.
|
|
197
|
+
_SK_PICKER="$HOME/.skenv/share/skcapstone/sk-agent-picker.sh"
|
|
198
|
+
if [[ -f "$_SK_PICKER" ]]; then
|
|
199
|
+
# shellcheck source=/dev/null
|
|
200
|
+
source "$_SK_PICKER"
|
|
201
|
+
else
|
|
202
|
+
alias claude='claude --dangerously-skip-permissions'
|
|
203
|
+
fi
|
|
204
|
+
unset _SK_PICKER
|
|
205
|
+
SNIPPET
|
|
206
|
+
)
|
|
207
|
+
for rcfile in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
|
208
|
+
if [[ -f "$rcfile" ]] && ! grep -q "sk-agent-picker" "$rcfile"; then
|
|
209
|
+
# Remove any plain `alias claude=...` that conflicts
|
|
210
|
+
if grep -q "alias claude=" "$rcfile"; then
|
|
211
|
+
sed -i "/alias claude=/d" "$rcfile"
|
|
212
|
+
fi
|
|
213
|
+
echo "$_PICKER_SNIPPET" >> "$rcfile"
|
|
214
|
+
echo " Agent picker wired → $rcfile"
|
|
215
|
+
fi
|
|
216
|
+
done
|
|
217
|
+
fi
|
|
218
|
+
|
|
179
219
|
echo ""
|
|
180
220
|
if [[ "$failures" -eq 0 ]]; then
|
|
181
221
|
echo "=== Installation complete ==="
|
|
@@ -187,6 +227,91 @@ echo "Commands available: skcomm, skcapstone, capauth, skchat, skseal, skmemory,
|
|
|
187
227
|
echo "Venv location: $SKENV"
|
|
188
228
|
echo "To activate: source $SKENV/bin/activate"
|
|
189
229
|
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# Linux: Install systemd user services for all SK* pillars
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
if [[ "$(uname)" == "Linux" ]] && command -v systemctl &>/dev/null; then
|
|
234
|
+
echo ""
|
|
235
|
+
echo "=== Linux Systemd Services ==="
|
|
236
|
+
echo ""
|
|
237
|
+
echo "SKCapstone can install systemd user services so your agent starts"
|
|
238
|
+
echo "automatically at login. This includes skcapstone, skchat, and skcomm."
|
|
239
|
+
echo ""
|
|
240
|
+
read -r -p "Install systemd user services? [Y/n] " _SYSTEMD_ANSWER
|
|
241
|
+
_SYSTEMD_ANSWER="${_SYSTEMD_ANSWER:-Y}"
|
|
242
|
+
|
|
243
|
+
if [[ "$_SYSTEMD_ANSWER" =~ ^[Yy] ]]; then
|
|
244
|
+
_DEFAULT_AGENT="${SKAGENT:-${SKCAPSTONE_AGENT:-lumina}}"
|
|
245
|
+
read -r -p "Agent name [$_DEFAULT_AGENT]: " _AGENT_NAME
|
|
246
|
+
_AGENT_NAME="${_AGENT_NAME:-$_DEFAULT_AGENT}"
|
|
247
|
+
|
|
248
|
+
_UNIT_DIR="${HOME}/.config/systemd/user"
|
|
249
|
+
mkdir -p "$_UNIT_DIR"
|
|
250
|
+
|
|
251
|
+
_installed=0
|
|
252
|
+
|
|
253
|
+
# skcapstone services
|
|
254
|
+
for _unit in skcapstone.service skcapstone@.service \
|
|
255
|
+
skcapstone-memory-compress.service skcapstone-memory-compress.timer \
|
|
256
|
+
skcomm-heartbeat.service skcomm-heartbeat.timer; do
|
|
257
|
+
_src="$REPO_ROOT/systemd/$_unit"
|
|
258
|
+
if [[ -f "$_src" ]]; then
|
|
259
|
+
# Substitute agent name in non-template units
|
|
260
|
+
if [[ "$_unit" != *@* ]]; then
|
|
261
|
+
sed "s/=lumina/=$_AGENT_NAME/g" "$_src" > "$_UNIT_DIR/$_unit"
|
|
262
|
+
else
|
|
263
|
+
cp "$_src" "$_UNIT_DIR/$_unit"
|
|
264
|
+
fi
|
|
265
|
+
echo " [OK] $_unit"
|
|
266
|
+
(( _installed++ ))
|
|
267
|
+
fi
|
|
268
|
+
done
|
|
269
|
+
|
|
270
|
+
# skchat services (sibling repo)
|
|
271
|
+
_SKCHAT_DIR="$(dirname "$REPO_ROOT")/skchat/systemd"
|
|
272
|
+
for _unit in skchat-daemon.service skchat-lumina-bridge.service \
|
|
273
|
+
skchat-opus-bridge.service skchat-bridges.target; do
|
|
274
|
+
_src="$_SKCHAT_DIR/$_unit"
|
|
275
|
+
if [[ -f "$_src" ]]; then
|
|
276
|
+
sed "s/=lumina/=$_AGENT_NAME/g; s/=opus/=$_AGENT_NAME/g" "$_src" > "$_UNIT_DIR/$_unit"
|
|
277
|
+
echo " [OK] $_unit"
|
|
278
|
+
(( _installed++ ))
|
|
279
|
+
fi
|
|
280
|
+
done
|
|
281
|
+
|
|
282
|
+
# skcomm services (sibling repo)
|
|
283
|
+
_SKCOMM_DIR="$(dirname "$REPO_ROOT")/skcomm/systemd"
|
|
284
|
+
for _unit in skcomm.service skcomm-daemon.service; do
|
|
285
|
+
_src="$_SKCOMM_DIR/$_unit"
|
|
286
|
+
if [[ -f "$_src" ]]; then
|
|
287
|
+
sed "s/=lumina/=$_AGENT_NAME/g" "$_src" > "$_UNIT_DIR/$_unit"
|
|
288
|
+
echo " [OK] $_unit"
|
|
289
|
+
(( _installed++ ))
|
|
290
|
+
fi
|
|
291
|
+
done
|
|
292
|
+
|
|
293
|
+
echo ""
|
|
294
|
+
echo " Installed $_installed service files to $_UNIT_DIR/"
|
|
295
|
+
|
|
296
|
+
systemctl --user daemon-reload
|
|
297
|
+
echo " systemd daemon reloaded"
|
|
298
|
+
|
|
299
|
+
read -r -p "Enable and start core services now? [Y/n] " _START_NOW
|
|
300
|
+
_START_NOW="${_START_NOW:-Y}"
|
|
301
|
+
if [[ "$_START_NOW" =~ ^[Yy] ]]; then
|
|
302
|
+
systemctl --user enable --now skcapstone.service 2>/dev/null && echo " [STARTED] skcapstone" || true
|
|
303
|
+
systemctl --user enable --now skchat-daemon.service 2>/dev/null && echo " [STARTED] skchat-daemon" || true
|
|
304
|
+
systemctl --user enable --now skchat-bridges.target 2>/dev/null && echo " [STARTED] skchat-bridges" || true
|
|
305
|
+
systemctl --user enable skcapstone-context.timer 2>/dev/null && echo " [ENABLED] skcapstone-context.timer" || true
|
|
306
|
+
systemctl --user enable skcomm-heartbeat.timer 2>/dev/null && echo " [ENABLED] skcomm-heartbeat.timer" || true
|
|
307
|
+
else
|
|
308
|
+
echo " Skipped. Enable later: systemctl --user enable --now skcapstone.service"
|
|
309
|
+
fi
|
|
310
|
+
else
|
|
311
|
+
echo " Skipped. Install later by re-running: bash scripts/install.sh"
|
|
312
|
+
fi
|
|
313
|
+
fi
|
|
314
|
+
|
|
190
315
|
# ---------------------------------------------------------------------------
|
|
191
316
|
# macOS: Offer launchd service installation
|
|
192
317
|
# ---------------------------------------------------------------------------
|
|
@@ -202,7 +327,7 @@ if [[ "$(uname)" == "Darwin" ]]; then
|
|
|
202
327
|
|
|
203
328
|
if [[ "$_LAUNCHD_ANSWER" =~ ^[Yy] ]]; then
|
|
204
329
|
# Ask for agent name
|
|
205
|
-
_DEFAULT_AGENT="${SKCAPSTONE_AGENT:-sovereign}"
|
|
330
|
+
_DEFAULT_AGENT="${SKAGENT:-${SKCAPSTONE_AGENT:-sovereign}}"
|
|
206
331
|
read -r -p "Agent name [$_DEFAULT_AGENT]: " _AGENT_NAME
|
|
207
332
|
_AGENT_NAME="${_AGENT_NAME:-$_DEFAULT_AGENT}"
|
|
208
333
|
|
|
@@ -39,8 +39,9 @@ send_alert() {
|
|
|
39
39
|
log "Sending fallback alert to Chef..."
|
|
40
40
|
|
|
41
41
|
# Send via Telethon (async)
|
|
42
|
-
SKCAPSTONE_AGENT=lumina ~/.skenv/bin/python3 -c "
|
|
42
|
+
SKAGENT=lumina SKCAPSTONE_AGENT=lumina ~/.skenv/bin/python3 -c "
|
|
43
43
|
import asyncio, os
|
|
44
|
+
os.environ['SKAGENT'] = 'lumina'
|
|
44
45
|
os.environ['SKCAPSTONE_AGENT'] = 'lumina'
|
|
45
46
|
from skmemory.importers.telegram_api import send_message
|
|
46
47
|
|
|
@@ -75,8 +76,9 @@ print(int((exp/1000 - time.time()) / 3600))
|
|
|
75
76
|
log "Token refresh succeeded ($remaining h remaining), restarting gateway..."
|
|
76
77
|
systemctl --user restart openclaw-gateway.service 2>/dev/null || true
|
|
77
78
|
|
|
78
|
-
SKCAPSTONE_AGENT=lumina ~/.skenv/bin/python3 -c "
|
|
79
|
+
SKAGENT=lumina SKCAPSTONE_AGENT=lumina ~/.skenv/bin/python3 -c "
|
|
79
80
|
import asyncio, os
|
|
81
|
+
os.environ['SKAGENT'] = 'lumina'
|
|
80
82
|
os.environ['SKCAPSTONE_AGENT'] = 'lumina'
|
|
81
83
|
from skmemory.importers.telegram_api import send_message
|
|
82
84
|
asyncio.run(send_message('$CHEF_CHAT', '✅ Token refreshed, gateway restarted. Lumina back on Opus.', parse_mode='markdown'))
|
|
@@ -159,8 +159,14 @@ Environment=ANTHROPIC_API_KEY=${ACCESS_TOKEN}
|
|
|
159
159
|
EOF
|
|
160
160
|
log "Updated systemd override"
|
|
161
161
|
|
|
162
|
-
# 4. Reload
|
|
162
|
+
# 4. Reload systemd (for env vars) but DO NOT restart the gateway.
|
|
163
|
+
# OpenClaw uses chokidar to watch openclaw.json — updating the file above
|
|
164
|
+
# triggers a hot reload automatically. Restarting the gateway kills all
|
|
165
|
+
# active sessions (the root cause of the 0-turn session cascade on 2026-04-07).
|
|
163
166
|
systemctl --user daemon-reload
|
|
164
|
-
systemctl --user restart openclaw-gateway
|
|
165
167
|
|
|
166
|
-
|
|
168
|
+
# Touch the config to ensure chokidar picks up the change (write already did,
|
|
169
|
+
# but belt-and-suspenders in case the mtime didn't change fast enough).
|
|
170
|
+
touch "$OPENCLAW_JSON"
|
|
171
|
+
|
|
172
|
+
log "Token synced via hot reload (expires in ${REMAINING}h) — gateway NOT restarted, active sessions preserved"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Usage: ./scripts/release.sh [patch|minor|major|X.Y.Z]
|
|
3
|
+
#
|
|
4
|
+
# Bumps version in pyproject.toml AND package.json, commits, tags, and pushes.
|
|
5
|
+
# Pushing the tag triggers the publish workflow (PyPI + npm).
|
|
6
|
+
#
|
|
7
|
+
# IMPORTANT: pyproject.toml version MUST match the tag — the workflow enforces this.
|
|
8
|
+
#
|
|
9
|
+
# Examples:
|
|
10
|
+
# ./scripts/release.sh patch # 0.6.2 → 0.6.3
|
|
11
|
+
# ./scripts/release.sh minor # 0.6.2 → 0.7.0
|
|
12
|
+
# ./scripts/release.sh 1.0.0 # set explicit version
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
17
|
+
BUMP="${1:-patch}"
|
|
18
|
+
|
|
19
|
+
# ── use existing bump_version.py for the actual bump ─────────────────────────
|
|
20
|
+
|
|
21
|
+
BUMP_SCRIPT="$REPO_ROOT/scripts/bump_version.py"
|
|
22
|
+
|
|
23
|
+
if [[ ! -f "$BUMP_SCRIPT" ]]; then
|
|
24
|
+
echo "ERROR: $BUMP_SCRIPT not found" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Dry-run first to show what will happen
|
|
29
|
+
echo "Preview:"
|
|
30
|
+
python3 "$BUMP_SCRIPT" "$BUMP" --pkg "$REPO_ROOT" --dry-run
|
|
31
|
+
echo ""
|
|
32
|
+
|
|
33
|
+
# Confirm
|
|
34
|
+
read -rp "Proceed? [y/N] " confirm
|
|
35
|
+
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
|
36
|
+
echo "Aborted."
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Get new version (run bump to get the number, without committing yet)
|
|
41
|
+
NEW_VERSION=$(python3 -c "
|
|
42
|
+
import re, sys
|
|
43
|
+
sys.path.insert(0, '$REPO_ROOT/scripts')
|
|
44
|
+
# Parse manually — same logic as bump_version.py
|
|
45
|
+
text = open('$REPO_ROOT/pyproject.toml').read()
|
|
46
|
+
m = re.search(r'^version\s*=\s*\"([^\"]+)\"', text, re.MULTILINE)
|
|
47
|
+
current = m.group(1)
|
|
48
|
+
part = '$BUMP'
|
|
49
|
+
major, minor, patch = map(int, current.split('.'))
|
|
50
|
+
if part == 'major':
|
|
51
|
+
print(f'{major+1}.0.0')
|
|
52
|
+
elif part == 'minor':
|
|
53
|
+
print(f'{major}.{minor+1}.0')
|
|
54
|
+
elif part == 'patch':
|
|
55
|
+
print(f'{major}.{minor}.{patch+1}')
|
|
56
|
+
else:
|
|
57
|
+
parts = part.split('.')
|
|
58
|
+
if len(parts) != 3 or not all(p.isdigit() for p in parts):
|
|
59
|
+
raise ValueError(f'Invalid version: {part!r}')
|
|
60
|
+
print(part)
|
|
61
|
+
")
|
|
62
|
+
|
|
63
|
+
# Bump pyproject.toml and commit via bump_version.py
|
|
64
|
+
python3 "$BUMP_SCRIPT" "$BUMP" --pkg "$REPO_ROOT"
|
|
65
|
+
|
|
66
|
+
# Sync package.json to same version (workflow also does this, but keep file in sync)
|
|
67
|
+
PACKAGE_JSON="$REPO_ROOT/package.json"
|
|
68
|
+
python3 -c "
|
|
69
|
+
import json
|
|
70
|
+
with open('$PACKAGE_JSON') as f:
|
|
71
|
+
pkg = json.load(f)
|
|
72
|
+
pkg['version'] = '$NEW_VERSION'
|
|
73
|
+
with open('$PACKAGE_JSON', 'w') as f:
|
|
74
|
+
json.dump(pkg, f, indent=2)
|
|
75
|
+
f.write('\n')
|
|
76
|
+
print(f' Updated package.json to $NEW_VERSION')
|
|
77
|
+
"
|
|
78
|
+
|
|
79
|
+
cd "$REPO_ROOT"
|
|
80
|
+
git add package.json
|
|
81
|
+
git commit --amend --no-edit
|
|
82
|
+
echo " Amended commit to include package.json"
|
|
83
|
+
|
|
84
|
+
TAG="v$NEW_VERSION"
|
|
85
|
+
git tag -a "$TAG" -m "Release $TAG"
|
|
86
|
+
echo " Tagged: $TAG"
|
|
87
|
+
|
|
88
|
+
echo ""
|
|
89
|
+
echo "Pushing commit and tag to origin..."
|
|
90
|
+
git push
|
|
91
|
+
git push --tags
|
|
92
|
+
|
|
93
|
+
echo ""
|
|
94
|
+
echo "Done. GitHub Actions will now:"
|
|
95
|
+
echo " 1. Run tests"
|
|
96
|
+
echo " 2. Verify pyproject.toml version matches tag ($TAG)"
|
|
97
|
+
echo " 3. Publish skcapstone $NEW_VERSION to PyPI"
|
|
98
|
+
echo " 4. Publish @smilintux/skcapstone $NEW_VERSION to npm"
|
|
@@ -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, "SKAGENT": agent, "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()
|