@smilintux/skcapstone 0.6.2 → 0.6.4

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 (70) hide show
  1. package/.github/workflows/publish.yml +1 -1
  2. package/CLAUDE.md +17 -0
  3. package/docs/CUSTOM_AGENT.md +40 -28
  4. package/docs/SOUL_SWAPPER.md +5 -5
  5. package/docs/hammertime-audit.md +402 -0
  6. package/openclaw-plugin/src/index.ts +2 -1
  7. package/package.json +1 -1
  8. package/pyproject.toml +2 -1
  9. package/scripts/install.sh +126 -1
  10. package/scripts/model-fallback-monitor.sh +4 -2
  11. package/scripts/refresh-anthropic-token.sh +9 -3
  12. package/scripts/release.sh +98 -0
  13. package/scripts/session-to-memory.py +1 -1
  14. package/scripts/sk-agent-picker.sh +237 -0
  15. package/scripts/telegram-catchup-all.sh +2 -1
  16. package/scripts/watch-anthropic-token.sh +12 -17
  17. package/src/skcapstone/__init__.py +34 -2
  18. package/src/skcapstone/cli/__init__.py +3 -1
  19. package/src/skcapstone/cli/_common.py +1 -0
  20. package/src/skcapstone/cli/context_cmd.py +16 -4
  21. package/src/skcapstone/cli/daemon.py +2 -1
  22. package/src/skcapstone/cli/joule_cmd.py +7 -3
  23. package/src/skcapstone/cli/memory.py +4 -2
  24. package/src/skcapstone/cli/register_cmd.py +19 -3
  25. package/src/skcapstone/cli/session.py +25 -0
  26. package/src/skcapstone/cli/setup.py +96 -30
  27. package/src/skcapstone/cli/soul.py +3 -3
  28. package/src/skcapstone/context_loader.py +9 -0
  29. package/src/skcapstone/coordination.py +9 -2
  30. package/src/skcapstone/daemon.py +22 -12
  31. package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
  32. package/src/skcapstone/defaults/claude/settings.json +74 -0
  33. package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
  34. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
  35. package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
  36. package/src/skcapstone/defaults/unhinged.json +13 -0
  37. package/src/skcapstone/discovery.py +5 -5
  38. package/src/skcapstone/doctor.py +4 -2
  39. package/src/skcapstone/dreaming.py +3 -1
  40. package/src/skcapstone/fuse_mount.py +3 -1
  41. package/src/skcapstone/housekeeping.py +3 -3
  42. package/src/skcapstone/install_wizard.py +131 -0
  43. package/src/skcapstone/mcp_launcher.py +14 -1
  44. package/src/skcapstone/mcp_server.py +6 -21
  45. package/src/skcapstone/mcp_tools/notification_tools.py +3 -1
  46. package/src/skcapstone/memory_engine.py +10 -3
  47. package/src/skcapstone/migrate_multi_agent.py +7 -6
  48. package/src/skcapstone/notifications.py +6 -2
  49. package/src/skcapstone/onboard.py +19 -8
  50. package/src/skcapstone/operator_link.py +164 -0
  51. package/src/skcapstone/pillars/consciousness.py +2 -1
  52. package/src/skcapstone/pillars/identity.py +51 -7
  53. package/src/skcapstone/pillars/memory.py +9 -3
  54. package/src/skcapstone/runtime.py +13 -3
  55. package/src/skcapstone/service_health.py +23 -10
  56. package/src/skcapstone/session_briefing.py +108 -0
  57. package/src/skcapstone/trust_graph.py +40 -5
  58. package/src/skcapstone/unified_search.py +11 -2
  59. package/systemd/skcapstone.service +4 -6
  60. package/systemd/skcapstone@.service +7 -8
  61. package/systemd/skcomm-heartbeat.service +5 -2
  62. package/tests/conftest.py +21 -0
  63. package/tests/test_agent_home_scaffold.py +34 -0
  64. package/tests/test_backup.py +2 -1
  65. package/tests/test_mcp_server.py +78 -33
  66. package/tests/test_multi_agent.py +31 -29
  67. package/tests/test_operator_link.py +78 -0
  68. package/tests/test_runtime.py +21 -0
  69. package/tests/test_session_briefing.py +130 -0
  70. package/tests/test_trust_graph.py +18 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smilintux/skcapstone",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
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",
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.0"
7
+ version = "0.6.4"
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"}
@@ -40,6 +40,7 @@ dependencies = [
40
40
  "pyyaml>=6.0",
41
41
  "rich>=13.0",
42
42
  "skmemory>=0.5.0",
43
+ "skskills>=0.1.1",
43
44
  ]
44
45
 
45
46
  [project.optional-dependencies]
@@ -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 and restart gateway
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
- log "Gateway restarted with fresh token (expires in ${REMAINING}h)"
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 SKCAPSTONE_AGENT="${SKCAPSTONE_AGENT:-lumina}"
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 OpenClaw
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=$(python3 -c "import json; print(json.load(open('$OPENCLAW_JSON'))['models']['providers']['anthropic']['apiKey'])" 2>/dev/null || echo "")
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. Update openclaw.json
51
- python3 << PYEOF
52
- import json
53
- with open('$OPENCLAW_JSON') as f:
54
- cfg = json.load(f)
55
- if 'anthropic' in cfg.get('models', {}).get('providers', {}):
56
- cfg['models']['providers']['anthropic']['apiKey'] = '$new_token'
57
- with open('$OPENCLAW_JSON', 'w') as f:
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)