@smilintux/skcapstone 0.6.2 → 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/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 +1 -1
- 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"}
|
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"
|
|
@@ -132,7 +132,7 @@ def save_to_skmemory(title: str, content: str, agent: str, tags: list[str]) -> b
|
|
|
132
132
|
],
|
|
133
133
|
capture_output=True,
|
|
134
134
|
timeout=30,
|
|
135
|
-
env={**os.environ, "SKCAPSTONE_AGENT": agent},
|
|
135
|
+
env={**os.environ, "SKAGENT": agent, "SKCAPSTONE_AGENT": agent},
|
|
136
136
|
)
|
|
137
137
|
if result.returncode != 0:
|
|
138
138
|
print(f" [skmemory error] {result.stderr.decode()[:200]}", file=sys.stderr)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# sk-agent-picker.sh — Sovereign agent picker for AI coding tools
|
|
3
|
+
#
|
|
4
|
+
# Source this file in ~/.bashrc or ~/.zshrc. It wraps `claude`, `codex`
|
|
5
|
+
# (OpenAI Codex CLI), and `opencode` with an agent-aware launcher that
|
|
6
|
+
# shows a numbered menu when multiple SK agents are configured.
|
|
7
|
+
#
|
|
8
|
+
# Also provides `skswitch` — a fast way to change the active agent for
|
|
9
|
+
# the current shell session (updates SKAGENT + legacy vars in one shot).
|
|
10
|
+
#
|
|
11
|
+
# Behaviour:
|
|
12
|
+
# - Zero agents found → launch tool normally (no SK home yet)
|
|
13
|
+
# - Exactly one agent → use it silently, no prompt
|
|
14
|
+
# - Multiple agents → numbered menu, default highlighted with →
|
|
15
|
+
# - SKAGENT is set → honour it, skip menu entirely
|
|
16
|
+
# - Pass --agent <name> → skip menu, use that agent directly
|
|
17
|
+
# - Any other args → forwarded to the underlying tool unchanged
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# claude # picker if multiple agents
|
|
21
|
+
# claude --agent lumina # direct launch
|
|
22
|
+
# SKAGENT=opus claude # env override
|
|
23
|
+
# skswitch lumina # change active agent for this shell
|
|
24
|
+
# skswitch # interactive picker
|
|
25
|
+
# codex # same picker logic
|
|
26
|
+
# opencode # same picker logic
|
|
27
|
+
#
|
|
28
|
+
# Source in shell config:
|
|
29
|
+
# source ~/.skenv/share/skcapstone/sk-agent-picker.sh
|
|
30
|
+
# Dev install:
|
|
31
|
+
# source ~/clawd/skcapstone-repos/skcapstone/scripts/sk-agent-picker.sh
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Core picker — returns chosen agent name on stdout, menu on stderr
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
_sk_pick_agent() {
|
|
37
|
+
local agents_dir="${SKCAPSTONE_HOME:-$HOME/.skcapstone}/agents"
|
|
38
|
+
local -a agents=()
|
|
39
|
+
|
|
40
|
+
if [[ -d "$agents_dir" ]]; then
|
|
41
|
+
while IFS= read -r entry; do
|
|
42
|
+
local name
|
|
43
|
+
name=$(basename "$entry")
|
|
44
|
+
# Skip template dirs, dotfiles, and non-directory entries
|
|
45
|
+
if [[ -d "$entry" && "$name" != *-template && "$name" != .* && "$name" != *.* ]]; then
|
|
46
|
+
agents+=("$name")
|
|
47
|
+
fi
|
|
48
|
+
done < <(find "$agents_dir" -mindepth 1 -maxdepth 1 -type d | sort)
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
local count="${#agents[@]}"
|
|
52
|
+
|
|
53
|
+
if [[ $count -eq 0 ]]; then
|
|
54
|
+
echo ""; return 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if [[ $count -eq 1 ]]; then
|
|
58
|
+
echo "${agents[0]}"; return 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Validate SKAGENT against actual agent list.
|
|
62
|
+
# If it's set but not in the list (stale env), fall back to first agent.
|
|
63
|
+
local env_agent="${SKAGENT:-${SKCAPSTONE_AGENT:-}}"
|
|
64
|
+
local default="${agents[0]}"
|
|
65
|
+
for agent in "${agents[@]}"; do
|
|
66
|
+
if [[ "$agent" == "$env_agent" ]]; then
|
|
67
|
+
default="$agent"
|
|
68
|
+
break
|
|
69
|
+
fi
|
|
70
|
+
done
|
|
71
|
+
|
|
72
|
+
# Multi-agent menu
|
|
73
|
+
echo "" >&2
|
|
74
|
+
echo " ╔══════════════════════════════════╗" >&2
|
|
75
|
+
echo " ║ SKCapstone — Choose an Agent ║" >&2
|
|
76
|
+
echo " ╚══════════════════════════════════╝" >&2
|
|
77
|
+
echo "" >&2
|
|
78
|
+
|
|
79
|
+
local i=1
|
|
80
|
+
for agent in "${agents[@]}"; do
|
|
81
|
+
local marker=" "
|
|
82
|
+
if [[ "$agent" == "$default" ]]; then
|
|
83
|
+
marker="→ "
|
|
84
|
+
fi
|
|
85
|
+
printf " %s%2d) %s\n" "$marker" "$i" "$agent" >&2
|
|
86
|
+
(( i++ ))
|
|
87
|
+
done
|
|
88
|
+
|
|
89
|
+
echo "" >&2
|
|
90
|
+
printf " Agent [1-%d, Enter = %s]: " "$count" "$default" >&2
|
|
91
|
+
|
|
92
|
+
local choice
|
|
93
|
+
read -r choice </dev/tty
|
|
94
|
+
|
|
95
|
+
# Empty → use default
|
|
96
|
+
if [[ -z "$choice" ]]; then
|
|
97
|
+
echo "$default"; return 0
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# Numeric
|
|
101
|
+
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= count )); then
|
|
102
|
+
echo "${agents[$((choice - 1))]}"; return 0
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Name typed directly
|
|
106
|
+
for agent in "${agents[@]}"; do
|
|
107
|
+
if [[ "$agent" == "$choice" ]]; then
|
|
108
|
+
echo "$agent"; return 0
|
|
109
|
+
fi
|
|
110
|
+
done
|
|
111
|
+
|
|
112
|
+
# Invalid — use list-validated default (not stale env), re-show options
|
|
113
|
+
printf "\n ⚠ Unknown agent '%s'. Valid agents:\n" "$choice" >&2
|
|
114
|
+
for agent in "${agents[@]}"; do
|
|
115
|
+
printf " %s\n" "$agent" >&2
|
|
116
|
+
done
|
|
117
|
+
printf " Using default: %s\n\n" "$default" >&2
|
|
118
|
+
echo "$default"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# Generic launcher used by all wrappers
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
_sk_launch() {
|
|
125
|
+
local tool="$1"; shift # the underlying binary (claude / codex / opencode)
|
|
126
|
+
local extra_flags="$1"; shift # tool-specific flags always appended (pass "" if none)
|
|
127
|
+
# remaining args collected below after parsing --agent
|
|
128
|
+
|
|
129
|
+
# Parse --agent <name> / --agent=<name> out of args first.
|
|
130
|
+
# SK_NO_PICKER=1 skips the menu entirely (for scripted/CI use).
|
|
131
|
+
local agent=""
|
|
132
|
+
local -a passthrough=()
|
|
133
|
+
local skip_next=0
|
|
134
|
+
|
|
135
|
+
for arg in "$@"; do
|
|
136
|
+
if [[ $skip_next -eq 1 ]]; then
|
|
137
|
+
agent="$arg"; skip_next=0; continue
|
|
138
|
+
fi
|
|
139
|
+
case "$arg" in
|
|
140
|
+
--agent) skip_next=1 ;;
|
|
141
|
+
--agent=*) agent="${arg#--agent=}" ;;
|
|
142
|
+
*) passthrough+=("$arg") ;;
|
|
143
|
+
esac
|
|
144
|
+
done
|
|
145
|
+
|
|
146
|
+
# --agent flag given → skip picker
|
|
147
|
+
# SK_NO_PICKER=1 → skip picker (scripted/CI use)
|
|
148
|
+
if [[ -z "$agent" && "${SK_NO_PICKER:-0}" != "1" ]]; then
|
|
149
|
+
agent=$(_sk_pick_agent)
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# Fallback: if picker returned empty (0 agents), just use SKAGENT
|
|
153
|
+
# or launch bare if that's also unset.
|
|
154
|
+
if [[ -z "$agent" ]]; then
|
|
155
|
+
agent="${SKAGENT:-${SKCAPSTONE_AGENT:-}}"
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
if [[ -n "$agent" ]]; then
|
|
159
|
+
printf " ▶ Starting %s as agent: %s\n\n" "$tool" "$agent" >&2
|
|
160
|
+
if [[ -n "$extra_flags" ]]; then
|
|
161
|
+
SKAGENT="$agent" SKCAPSTONE_AGENT="$agent" SKMEMORY_AGENT="$agent" command "$tool" $extra_flags "${passthrough[@]}"
|
|
162
|
+
else
|
|
163
|
+
SKAGENT="$agent" SKCAPSTONE_AGENT="$agent" SKMEMORY_AGENT="$agent" command "$tool" "${passthrough[@]}"
|
|
164
|
+
fi
|
|
165
|
+
else
|
|
166
|
+
if [[ -n "$extra_flags" ]]; then
|
|
167
|
+
command "$tool" $extra_flags "${passthrough[@]}"
|
|
168
|
+
else
|
|
169
|
+
command "$tool" "${passthrough[@]}"
|
|
170
|
+
fi
|
|
171
|
+
fi
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# skswitch — change the active agent for the current shell session
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
function skswitch {
|
|
178
|
+
local agent="$1"
|
|
179
|
+
|
|
180
|
+
if [[ -z "$agent" ]]; then
|
|
181
|
+
# No argument — show interactive picker
|
|
182
|
+
agent=$(_sk_pick_agent)
|
|
183
|
+
if [[ -z "$agent" ]]; then
|
|
184
|
+
echo "No agents found in ${SKCAPSTONE_HOME:-$HOME/.skcapstone}/agents/" >&2
|
|
185
|
+
return 1
|
|
186
|
+
fi
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
# Validate agent directory exists
|
|
190
|
+
local agent_dir="${SKCAPSTONE_HOME:-$HOME/.skcapstone}/agents/$agent"
|
|
191
|
+
if [[ ! -d "$agent_dir" ]]; then
|
|
192
|
+
echo "Agent not found: $agent" >&2
|
|
193
|
+
echo "Available agents:" >&2
|
|
194
|
+
local agents_dir="${SKCAPSTONE_HOME:-$HOME/.skcapstone}/agents"
|
|
195
|
+
if [[ -d "$agents_dir" ]]; then
|
|
196
|
+
find "$agents_dir" -mindepth 1 -maxdepth 1 -type d ! -name '*-template' ! -name '.*' -printf ' %f\n' | sort >&2
|
|
197
|
+
fi
|
|
198
|
+
return 1
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
export SKAGENT="$agent"
|
|
202
|
+
export SKCAPSTONE_AGENT="$agent"
|
|
203
|
+
export SKMEMORY_AGENT="$agent"
|
|
204
|
+
echo "Switched to agent: $agent"
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# Per-tool wrapper functions
|
|
209
|
+
# Must unalias first — an active alias with the same name causes bash to
|
|
210
|
+
# expand it during function-definition parsing, producing a syntax error.
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
unalias claude 2>/dev/null || true
|
|
213
|
+
unalias codex 2>/dev/null || true
|
|
214
|
+
unalias opencode 2>/dev/null || true
|
|
215
|
+
|
|
216
|
+
# claude (Claude Code CLI)
|
|
217
|
+
function claude {
|
|
218
|
+
_sk_launch claude "--dangerously-skip-permissions" "$@"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# codex (OpenAI Codex CLI — https://github.com/openai/codex)
|
|
222
|
+
function codex {
|
|
223
|
+
_sk_launch codex "--full-auto" "$@"
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# opencode (opencode.ai)
|
|
227
|
+
function opencode {
|
|
228
|
+
_sk_launch opencode "" "$@"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# Export so sub-shells (tmux panes, etc.) inherit the functions
|
|
232
|
+
export -f _sk_pick_agent 2>/dev/null || true
|
|
233
|
+
export -f _sk_launch 2>/dev/null || true
|
|
234
|
+
export -f skswitch 2>/dev/null || true
|
|
235
|
+
export -f claude 2>/dev/null || true
|
|
236
|
+
export -f codex 2>/dev/null || true
|
|
237
|
+
export -f opencode 2>/dev/null || true
|
|
@@ -22,7 +22,8 @@ set -uo pipefail # no -e: individual group failures shouldn't stop the batch
|
|
|
22
22
|
SKENV="${HOME}/.skenv/bin"
|
|
23
23
|
SKCAPSTONE="${SKENV}/skcapstone"
|
|
24
24
|
CONFIG="${HOME}/.skcapstone/agents/lumina/config/telegram.yaml"
|
|
25
|
-
export
|
|
25
|
+
export SKAGENT="${SKAGENT:-lumina}"
|
|
26
|
+
export SKCAPSTONE_AGENT="${SKAGENT}"
|
|
26
27
|
export PATH="${SKENV}:${PATH}"
|
|
27
28
|
|
|
28
29
|
# Parse args
|
|
@@ -36,9 +36,10 @@ sync_token() {
|
|
|
36
36
|
local expires_in
|
|
37
37
|
expires_in=$(python3 -c "import json,time; print(f'{(json.load(open(\"$CREDS\"))[\"claudeAiOauth\"][\"expiresAt\"]/1000 - time.time())/3600:.1f}h')" 2>/dev/null || echo "unknown")
|
|
38
38
|
|
|
39
|
-
# Read current token from
|
|
39
|
+
# Read current token from credentials file (track changes by comparing with last known)
|
|
40
|
+
local state_file="$HOME/.skcapstone/agents/lumina/logs/anthropic-token.last"
|
|
40
41
|
local current_token
|
|
41
|
-
current_token=$(
|
|
42
|
+
current_token=$(cat "$state_file" 2>/dev/null || echo "")
|
|
42
43
|
|
|
43
44
|
if [ "$new_token" = "$current_token" ]; then
|
|
44
45
|
log "Token unchanged (expires in $expires_in)"
|
|
@@ -47,20 +48,14 @@ sync_token() {
|
|
|
47
48
|
|
|
48
49
|
log "Token changed! Syncing... (new token expires in $expires_in)"
|
|
49
50
|
|
|
50
|
-
# 1.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
json.dump(cfg, f, indent=2)
|
|
59
|
-
f.write('\n')
|
|
60
|
-
PYEOF
|
|
61
|
-
log "Updated openclaw.json"
|
|
62
|
-
|
|
63
|
-
# 2. Update .env
|
|
51
|
+
# 1. Save new token to state file
|
|
52
|
+
echo "$new_token" > "$state_file"
|
|
53
|
+
log "State file updated"
|
|
54
|
+
|
|
55
|
+
# NOTE: anthropic provider removed from openclaw.json — all Claude models
|
|
56
|
+
# now route through claude-code proxy (port 18782). No openclaw.json update needed.
|
|
57
|
+
|
|
58
|
+
# 2. Update .env (kept for any scripts that source it)
|
|
64
59
|
if grep -q "^ANTHROPIC_API_KEY=" "$OPENCLAW_ENV" 2>/dev/null; then
|
|
65
60
|
sed -i "s|^ANTHROPIC_API_KEY=.*|ANTHROPIC_API_KEY=$new_token|" "$OPENCLAW_ENV"
|
|
66
61
|
else
|
|
@@ -68,7 +63,7 @@ PYEOF
|
|
|
68
63
|
fi
|
|
69
64
|
log "Updated .env"
|
|
70
65
|
|
|
71
|
-
# 3. Update systemd override
|
|
66
|
+
# 3. Update systemd override (ANTHROPIC_API_KEY kept for claude-code-api server)
|
|
72
67
|
if [ -f "$OVERRIDE_CONF" ]; then
|
|
73
68
|
local nvidia_key
|
|
74
69
|
nvidia_key=$(grep "NVIDIA_API_KEY=" "$OVERRIDE_CONF" 2>/dev/null | sed 's/.*NVIDIA_API_KEY=//' || true)
|
|
@@ -25,11 +25,38 @@ def _default_home() -> str:
|
|
|
25
25
|
return os.path.expanduser("~/.skcapstone")
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def _detect_active_agent(root: str | None = None) -> str | None:
|
|
29
|
+
"""Best-effort active agent discovery.
|
|
30
|
+
|
|
31
|
+
Resolution order:
|
|
32
|
+
1. Explicit SKCAPSTONE_AGENT environment variable
|
|
33
|
+
2. First non-template directory under ~/.skcapstone/agents
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The active agent name if one can be resolved, else None.
|
|
37
|
+
"""
|
|
38
|
+
env_agent = (os.environ.get("SKAGENT") or os.environ.get("SKCAPSTONE_AGENT", "")).strip()
|
|
39
|
+
if env_agent:
|
|
40
|
+
return env_agent
|
|
41
|
+
|
|
42
|
+
base = Path(root or os.environ.get("SKCAPSTONE_HOME", _default_home())).expanduser()
|
|
43
|
+
agents_dir = base / "agents"
|
|
44
|
+
if not agents_dir.exists():
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
candidates = sorted(
|
|
48
|
+
entry.name
|
|
49
|
+
for entry in agents_dir.iterdir()
|
|
50
|
+
if entry.is_dir() and not entry.name.endswith("-template")
|
|
51
|
+
)
|
|
52
|
+
return candidates[0] if candidates else None
|
|
53
|
+
|
|
54
|
+
|
|
28
55
|
# Root of the skcapstone tree (shared infra lives here)
|
|
29
56
|
AGENT_HOME = os.environ.get("SKCAPSTONE_HOME", _default_home())
|
|
30
57
|
|
|
31
58
|
# Which agent this process is running as (set by daemon/connector)
|
|
32
|
-
SKCAPSTONE_AGENT =
|
|
59
|
+
SKCAPSTONE_AGENT = _detect_active_agent() or ""
|
|
33
60
|
|
|
34
61
|
# Default daemon port
|
|
35
62
|
DEFAULT_PORT = int(os.environ.get("SKCAPSTONE_PORT", "9383"))
|
|
@@ -59,13 +86,18 @@ def agent_home(agent_name: str | None = None) -> Path:
|
|
|
59
86
|
Returns:
|
|
60
87
|
Path to the agent-specific home directory.
|
|
61
88
|
"""
|
|
62
|
-
name = agent_name or SKCAPSTONE_AGENT
|
|
89
|
+
name = agent_name or SKCAPSTONE_AGENT or _detect_active_agent()
|
|
63
90
|
root = Path(AGENT_HOME).expanduser()
|
|
64
91
|
if name:
|
|
65
92
|
return root / "agents" / name
|
|
66
93
|
return root
|
|
67
94
|
|
|
68
95
|
|
|
96
|
+
def active_agent_name() -> str | None:
|
|
97
|
+
"""Return the currently active agent name, if one can be resolved."""
|
|
98
|
+
return SKCAPSTONE_AGENT or _detect_active_agent()
|
|
99
|
+
|
|
100
|
+
|
|
69
101
|
def shared_home() -> Path:
|
|
70
102
|
"""Return the shared root directory (~/.skcapstone/).
|
|
71
103
|
|