@smilintux/skcapstone 0.2.6 → 0.3.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/README.md +61 -0
- package/docs/CUSTOM_AGENT.md +184 -0
- package/docs/GETTING_STARTED.md +3 -0
- package/openclaw-plugin/src/index.ts +75 -4
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/archive-sessions.sh +72 -0
- package/scripts/install.ps1 +2 -1
- package/scripts/install.sh +2 -1
- package/scripts/nvidia-proxy.mjs +727 -0
- package/scripts/telegram-catchup-all.sh +136 -0
- package/src/skcapstone/__init__.py +70 -1
- package/src/skcapstone/agent_card.py +4 -1
- package/src/skcapstone/blueprint_registry.py +78 -0
- package/src/skcapstone/blueprints/builtins/itil-operations.yaml +40 -0
- package/src/skcapstone/cli/__init__.py +2 -0
- package/src/skcapstone/cli/_common.py +5 -5
- package/src/skcapstone/cli/card.py +36 -5
- package/src/skcapstone/cli/config_cmd.py +53 -1
- package/src/skcapstone/cli/itil.py +434 -0
- package/src/skcapstone/cli/peer.py +3 -1
- package/src/skcapstone/cli/peers_dir.py +3 -1
- package/src/skcapstone/cli/preflight_cmd.py +4 -0
- package/src/skcapstone/cli/skills_cmd.py +120 -24
- package/src/skcapstone/cli/soul.py +47 -24
- package/src/skcapstone/cli/status.py +17 -11
- package/src/skcapstone/cli/usage_cmd.py +7 -2
- package/src/skcapstone/consciousness_config.py +27 -0
- package/src/skcapstone/coordination.py +1 -0
- package/src/skcapstone/daemon.py +28 -9
- package/src/skcapstone/defaults/lumina/manifest.json +1 -1
- package/src/skcapstone/doctor.py +115 -0
- package/src/skcapstone/dreaming.py +761 -0
- package/src/skcapstone/itil.py +1104 -0
- package/src/skcapstone/mcp_server.py +258 -0
- package/src/skcapstone/mcp_tools/__init__.py +2 -0
- package/src/skcapstone/mcp_tools/gtd_tools.py +1 -1
- package/src/skcapstone/mcp_tools/itil_tools.py +657 -0
- package/src/skcapstone/mcp_tools/notification_tools.py +12 -11
- package/src/skcapstone/notifications.py +40 -27
- package/src/skcapstone/onboard.py +46 -0
- package/src/skcapstone/pillars/sync.py +11 -4
- package/src/skcapstone/register.py +8 -0
- package/src/skcapstone/scheduled_tasks.py +107 -0
- package/src/skcapstone/service_health.py +81 -2
- package/src/skcapstone/soul.py +19 -0
- package/systemd/skcapstone.service +5 -6
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# telegram-catchup-all.sh — Import all configured Telegram groups into SKMemory
|
|
3
|
+
#
|
|
4
|
+
# Reads groups from ~/.skcapstone/agents/lumina/config/telegram.yaml
|
|
5
|
+
# and runs `skcapstone telegram catchup` for each enabled group.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bash scripts/telegram-catchup-all.sh [--since YYYY-MM-DD] [--limit N] [--group NAME]
|
|
9
|
+
#
|
|
10
|
+
# Examples:
|
|
11
|
+
# bash scripts/telegram-catchup-all.sh # All groups, last 2000 msgs
|
|
12
|
+
# bash scripts/telegram-catchup-all.sh --since 2026-03-01 # All groups since March 1
|
|
13
|
+
# bash scripts/telegram-catchup-all.sh --group brother-john # Just one group
|
|
14
|
+
#
|
|
15
|
+
# Requires:
|
|
16
|
+
# - TELEGRAM_API_ID and TELEGRAM_API_HASH environment variables
|
|
17
|
+
# - ~/.skenv/bin/skcapstone on PATH
|
|
18
|
+
# - Telethon installed in ~/.skenv/
|
|
19
|
+
|
|
20
|
+
set -uo pipefail # no -e: individual group failures shouldn't stop the batch
|
|
21
|
+
|
|
22
|
+
SKENV="${HOME}/.skenv/bin"
|
|
23
|
+
SKCAPSTONE="${SKENV}/skcapstone"
|
|
24
|
+
CONFIG="${HOME}/.skcapstone/agents/lumina/config/telegram.yaml"
|
|
25
|
+
export SKCAPSTONE_AGENT="${SKCAPSTONE_AGENT:-lumina}"
|
|
26
|
+
export PATH="${SKENV}:${PATH}"
|
|
27
|
+
|
|
28
|
+
# Parse args
|
|
29
|
+
SINCE=""
|
|
30
|
+
LIMIT="2000"
|
|
31
|
+
ONLY_GROUP=""
|
|
32
|
+
|
|
33
|
+
while [[ $# -gt 0 ]]; do
|
|
34
|
+
case "$1" in
|
|
35
|
+
--since) SINCE="$2"; shift 2 ;;
|
|
36
|
+
--limit) LIMIT="$2"; shift 2 ;;
|
|
37
|
+
--group) ONLY_GROUP="$2"; shift 2 ;;
|
|
38
|
+
*) echo "Unknown arg: $1"; exit 1 ;;
|
|
39
|
+
esac
|
|
40
|
+
done
|
|
41
|
+
|
|
42
|
+
# Check prerequisites
|
|
43
|
+
if [[ -z "${TELEGRAM_API_ID:-}" || -z "${TELEGRAM_API_HASH:-}" ]]; then
|
|
44
|
+
echo "ERROR: TELEGRAM_API_ID and TELEGRAM_API_HASH must be set."
|
|
45
|
+
echo "Get them from https://my.telegram.org"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
if [[ ! -f "$CONFIG" ]]; then
|
|
50
|
+
echo "ERROR: Config not found: $CONFIG"
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Parse groups from YAML (simple grep — no yq dependency)
|
|
55
|
+
echo "=== Telegram Catch-Up All ==="
|
|
56
|
+
echo "Config: $CONFIG"
|
|
57
|
+
echo "Agent: $SKCAPSTONE_AGENT"
|
|
58
|
+
echo "Limit: $LIMIT"
|
|
59
|
+
[[ -n "$SINCE" ]] && echo "Since: $SINCE"
|
|
60
|
+
[[ -n "$ONLY_GROUP" ]] && echo "Only group: $ONLY_GROUP"
|
|
61
|
+
echo ""
|
|
62
|
+
|
|
63
|
+
# Extract group entries: name, chat ID, tags, enabled status
|
|
64
|
+
SUCCESS=0
|
|
65
|
+
FAILED=0
|
|
66
|
+
SKIPPED=0
|
|
67
|
+
|
|
68
|
+
current_name=""
|
|
69
|
+
current_chat=""
|
|
70
|
+
current_tags=""
|
|
71
|
+
current_enabled=""
|
|
72
|
+
|
|
73
|
+
process_group() {
|
|
74
|
+
local name="$1" chat="$2" tags="$3" enabled="$4"
|
|
75
|
+
|
|
76
|
+
if [[ "$enabled" != "true" ]]; then
|
|
77
|
+
echo " SKIP $name (disabled)"
|
|
78
|
+
SKIPPED=$((SKIPPED + 1))
|
|
79
|
+
return
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
if [[ -n "$ONLY_GROUP" && "$name" != *"$ONLY_GROUP"* ]]; then
|
|
83
|
+
SKIPPED=$((SKIPPED + 1))
|
|
84
|
+
return
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
echo -n " IMPORTING $name (chat: $chat) ... "
|
|
88
|
+
|
|
89
|
+
local cmd="$SKCAPSTONE telegram catchup $chat --limit $LIMIT --min-length 20"
|
|
90
|
+
[[ -n "$SINCE" ]] && cmd="$cmd --since $SINCE"
|
|
91
|
+
[[ -n "$tags" ]] && cmd="$cmd --tags $tags"
|
|
92
|
+
|
|
93
|
+
if eval "$cmd" > /tmp/telegram-catchup-$name.log 2>&1; then
|
|
94
|
+
echo "OK"
|
|
95
|
+
SUCCESS=$((SUCCESS + 1))
|
|
96
|
+
else
|
|
97
|
+
echo "FAILED (see /tmp/telegram-catchup-$name.log)"
|
|
98
|
+
FAILED=$((FAILED + 1))
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# Rate limit — avoid hitting Telegram flood control
|
|
102
|
+
sleep 3
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Parse the YAML manually
|
|
106
|
+
while IFS= read -r line; do
|
|
107
|
+
# Detect new group entry
|
|
108
|
+
if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*(.*) ]]; then
|
|
109
|
+
# Process previous group if we have one
|
|
110
|
+
if [[ -n "$current_name" ]]; then
|
|
111
|
+
process_group "$current_name" "$current_chat" "$current_tags" "$current_enabled"
|
|
112
|
+
fi
|
|
113
|
+
current_name="${BASH_REMATCH[1]}"
|
|
114
|
+
current_chat=""
|
|
115
|
+
current_tags=""
|
|
116
|
+
current_enabled="true"
|
|
117
|
+
elif [[ "$line" =~ ^[[:space:]]*chat:[[:space:]]*\"?([0-9]+)\"? ]]; then
|
|
118
|
+
current_chat="${BASH_REMATCH[1]}"
|
|
119
|
+
elif [[ "$line" =~ ^[[:space:]]*tags:[[:space:]]*\[(.*)\] ]]; then
|
|
120
|
+
# Convert YAML list to comma-separated
|
|
121
|
+
current_tags=$(echo "${BASH_REMATCH[1]}" | sed 's/,/ /g' | tr -s ' ' ',' | sed 's/^,//;s/,$//')
|
|
122
|
+
elif [[ "$line" =~ ^[[:space:]]*enabled:[[:space:]]*(.*) ]]; then
|
|
123
|
+
current_enabled="${BASH_REMATCH[1]}"
|
|
124
|
+
fi
|
|
125
|
+
done < "$CONFIG"
|
|
126
|
+
|
|
127
|
+
# Process last group
|
|
128
|
+
if [[ -n "$current_name" ]]; then
|
|
129
|
+
process_group "$current_name" "$current_chat" "$current_tags" "$current_enabled"
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
echo ""
|
|
133
|
+
echo "=== Done ==="
|
|
134
|
+
echo " Success: $SUCCESS"
|
|
135
|
+
echo " Failed: $FAILED"
|
|
136
|
+
echo " Skipped: $SKIPPED"
|
|
@@ -11,7 +11,7 @@ import os
|
|
|
11
11
|
import platform
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
-
__version__ = "0.
|
|
14
|
+
__version__ = "0.4.4"
|
|
15
15
|
__author__ = "smilinTux"
|
|
16
16
|
|
|
17
17
|
|
|
@@ -76,3 +76,72 @@ def shared_home() -> Path:
|
|
|
76
76
|
Path to the shared skcapstone root.
|
|
77
77
|
"""
|
|
78
78
|
return Path(AGENT_HOME).expanduser()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def ensure_skeleton(agent_name: str | None = None) -> None:
|
|
82
|
+
"""Create all expected directories for the shared root and agent home.
|
|
83
|
+
|
|
84
|
+
Idempotent — safe to call multiple times. Creates any missing
|
|
85
|
+
directories so that all CLI commands and services find the paths
|
|
86
|
+
they expect.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
agent_name: Agent name (defaults to SKCAPSTONE_AGENT).
|
|
90
|
+
"""
|
|
91
|
+
root = shared_home()
|
|
92
|
+
name = agent_name or SKCAPSTONE_AGENT
|
|
93
|
+
agent_dir = root / "agents" / name
|
|
94
|
+
|
|
95
|
+
# Shared root directories
|
|
96
|
+
for d in (
|
|
97
|
+
root / "config",
|
|
98
|
+
root / "identity",
|
|
99
|
+
root / "security",
|
|
100
|
+
root / "skills",
|
|
101
|
+
root / "heartbeats",
|
|
102
|
+
root / "peers",
|
|
103
|
+
root / "coordination" / "tasks",
|
|
104
|
+
root / "coordination" / "agents",
|
|
105
|
+
root / "logs",
|
|
106
|
+
root / "comms" / "inbox",
|
|
107
|
+
root / "comms" / "outbox",
|
|
108
|
+
root / "comms" / "archive",
|
|
109
|
+
root / "archive",
|
|
110
|
+
root / "deployments",
|
|
111
|
+
root / "docs",
|
|
112
|
+
root / "metrics",
|
|
113
|
+
root / "memory",
|
|
114
|
+
root / "sync" / "outbox",
|
|
115
|
+
root / "sync" / "inbox",
|
|
116
|
+
root / "sync" / "archive",
|
|
117
|
+
root / "trust" / "febs",
|
|
118
|
+
):
|
|
119
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
|
|
121
|
+
# Per-agent directories
|
|
122
|
+
for d in (
|
|
123
|
+
agent_dir / "memory" / "short-term",
|
|
124
|
+
agent_dir / "memory" / "mid-term",
|
|
125
|
+
agent_dir / "memory" / "long-term",
|
|
126
|
+
agent_dir / "soul" / "installed",
|
|
127
|
+
agent_dir / "wallet",
|
|
128
|
+
agent_dir / "seeds",
|
|
129
|
+
agent_dir / "identity",
|
|
130
|
+
agent_dir / "config",
|
|
131
|
+
agent_dir / "logs",
|
|
132
|
+
agent_dir / "security",
|
|
133
|
+
agent_dir / "cloud9",
|
|
134
|
+
agent_dir / "trust" / "febs",
|
|
135
|
+
agent_dir / "sync" / "outbox",
|
|
136
|
+
agent_dir / "sync" / "inbox",
|
|
137
|
+
agent_dir / "sync" / "archive",
|
|
138
|
+
agent_dir / "reflections",
|
|
139
|
+
agent_dir / "improvements",
|
|
140
|
+
agent_dir / "scripts",
|
|
141
|
+
agent_dir / "cron",
|
|
142
|
+
agent_dir / "archive",
|
|
143
|
+
agent_dir / "comms" / "inbox",
|
|
144
|
+
agent_dir / "comms" / "outbox",
|
|
145
|
+
agent_dir / "comms" / "archive",
|
|
146
|
+
):
|
|
147
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
@@ -212,7 +212,10 @@ class AgentCard(BaseModel):
|
|
|
212
212
|
content = self.content_hash().encode("utf-8")
|
|
213
213
|
pgp_message = pgpy.PGPMessage.new(content, cleartext=False)
|
|
214
214
|
|
|
215
|
-
|
|
215
|
+
if key.is_protected:
|
|
216
|
+
with key.unlock(passphrase):
|
|
217
|
+
sig = key.sign(pgp_message)
|
|
218
|
+
else:
|
|
216
219
|
sig = key.sign(pgp_message)
|
|
217
220
|
|
|
218
221
|
self.signature = str(sig)
|
|
@@ -355,3 +355,81 @@ class BlueprintRegistryClient:
|
|
|
355
355
|
return True
|
|
356
356
|
except BlueprintRegistryError:
|
|
357
357
|
return False
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# --------------------------------------------------------------------------
|
|
361
|
+
# GitHub-based fallback — reads blueprints directly from the repo
|
|
362
|
+
# --------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
_GITHUB_API_URL = "https://api.github.com/repos/smilinTux/soul-blueprints/contents/blueprints"
|
|
365
|
+
_GITHUB_RAW_URL = "https://raw.githubusercontent.com/smilinTux/soul-blueprints/main/blueprints"
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _fetch_github_blueprints(query: str = "") -> Optional[list[dict[str, Any]]]:
|
|
369
|
+
"""Fetch blueprint listings from the soul-blueprints GitHub repo.
|
|
370
|
+
|
|
371
|
+
Uses the GitHub Contents API to list category directories, then
|
|
372
|
+
fetches file names from each. Lightweight header parsing is done
|
|
373
|
+
via raw file fetch for descriptions.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
query: Optional search filter (case-insensitive).
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
List of blueprint dicts, or None on failure.
|
|
380
|
+
"""
|
|
381
|
+
try:
|
|
382
|
+
# Get top-level categories
|
|
383
|
+
req = urllib.request.Request(
|
|
384
|
+
_GITHUB_API_URL,
|
|
385
|
+
headers={"User-Agent": "skcapstone", "Accept": "application/json"},
|
|
386
|
+
)
|
|
387
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
388
|
+
categories = json.loads(resp.read().decode("utf-8"))
|
|
389
|
+
except Exception as exc:
|
|
390
|
+
logger.debug("GitHub blueprint fetch failed: %s", exc)
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
blueprints: list[dict[str, Any]] = []
|
|
394
|
+
q = query.lower()
|
|
395
|
+
|
|
396
|
+
for cat_entry in categories:
|
|
397
|
+
if cat_entry.get("type") != "dir":
|
|
398
|
+
continue
|
|
399
|
+
cat_name = cat_entry["name"]
|
|
400
|
+
|
|
401
|
+
# Fetch files in this category
|
|
402
|
+
try:
|
|
403
|
+
cat_req = urllib.request.Request(
|
|
404
|
+
cat_entry["url"],
|
|
405
|
+
headers={"User-Agent": "skcapstone", "Accept": "application/json"},
|
|
406
|
+
)
|
|
407
|
+
with urllib.request.urlopen(cat_req, timeout=10) as resp:
|
|
408
|
+
files = json.loads(resp.read().decode("utf-8"))
|
|
409
|
+
except Exception:
|
|
410
|
+
continue
|
|
411
|
+
|
|
412
|
+
for file_entry in files:
|
|
413
|
+
fname = file_entry.get("name", "")
|
|
414
|
+
if not fname.lower().endswith((".md", ".yaml", ".yml")):
|
|
415
|
+
continue
|
|
416
|
+
if fname.lower() in ("readme.md", "index.html"):
|
|
417
|
+
continue
|
|
418
|
+
|
|
419
|
+
stem = fname.rsplit(".", 1)[0]
|
|
420
|
+
slug = stem.lower().replace("_", "-").replace(" ", "-")
|
|
421
|
+
display = stem.replace("_", " ").replace("-", " ").title()
|
|
422
|
+
|
|
423
|
+
# Apply search filter
|
|
424
|
+
if q and q not in slug and q not in cat_name.lower() and q not in display.lower():
|
|
425
|
+
continue
|
|
426
|
+
|
|
427
|
+
blueprints.append({
|
|
428
|
+
"name": slug,
|
|
429
|
+
"display_name": display,
|
|
430
|
+
"category": cat_name,
|
|
431
|
+
"source": "github",
|
|
432
|
+
"raw_url": f"{_GITHUB_RAW_URL}/{cat_name}/{fname}",
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
return sorted(blueprints, key=lambda d: (d["category"], d["name"]))
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: "ITIL Operations"
|
|
2
|
+
slug: "itil-operations"
|
|
3
|
+
version: "1.0.0"
|
|
4
|
+
description: "ITIL service management — incident, problem, and change lifecycle with SLA monitoring and continuous improvement."
|
|
5
|
+
icon: "🔄"
|
|
6
|
+
author: "smilinTux"
|
|
7
|
+
|
|
8
|
+
agents:
|
|
9
|
+
deming:
|
|
10
|
+
role: ops
|
|
11
|
+
model: reason
|
|
12
|
+
model_name: "deepseek-r1:32b"
|
|
13
|
+
description: "ITIL expert — incident triage, problem analysis, change management, SLA monitoring, and blameless postmortems."
|
|
14
|
+
vm_type: container
|
|
15
|
+
resources:
|
|
16
|
+
memory: "4g"
|
|
17
|
+
cores: 2
|
|
18
|
+
disk: "20g"
|
|
19
|
+
soul_blueprint: "souls/deming.yaml"
|
|
20
|
+
skills: [incident-management, problem-analysis, change-management, kedb, sla-monitoring, root-cause-analysis]
|
|
21
|
+
|
|
22
|
+
default_provider: local
|
|
23
|
+
estimated_cost: "$0 (local)"
|
|
24
|
+
|
|
25
|
+
network:
|
|
26
|
+
mesh_vpn: tailscale
|
|
27
|
+
discovery: skref_registry
|
|
28
|
+
|
|
29
|
+
storage:
|
|
30
|
+
skref_vault: "team-itil-ops"
|
|
31
|
+
memory_backend: filesystem
|
|
32
|
+
memory_sync: true
|
|
33
|
+
|
|
34
|
+
coordination:
|
|
35
|
+
queen: lumina
|
|
36
|
+
pattern: supervisor
|
|
37
|
+
heartbeat: "5m"
|
|
38
|
+
escalation: chef
|
|
39
|
+
|
|
40
|
+
tags: [ops, itil, incident-management, problem-analysis, change-management, sla-monitoring, continuous-improvement]
|
|
@@ -86,6 +86,7 @@ from .search_cmd import register_search_commands
|
|
|
86
86
|
from .mood_cmd import register_mood_commands
|
|
87
87
|
from .register_cmd import register_register_commands
|
|
88
88
|
from .gtd import register_gtd_commands
|
|
89
|
+
from .itil import register_itil_commands
|
|
89
90
|
from .skseed import register_skseed_commands
|
|
90
91
|
from .service_cmd import register_service_commands
|
|
91
92
|
from .telegram import register_telegram_commands
|
|
@@ -138,6 +139,7 @@ register_search_commands(main)
|
|
|
138
139
|
register_mood_commands(main)
|
|
139
140
|
register_register_commands(main)
|
|
140
141
|
register_gtd_commands(main)
|
|
142
|
+
register_itil_commands(main)
|
|
141
143
|
register_skseed_commands(main)
|
|
142
144
|
register_service_commands(main)
|
|
143
145
|
register_telegram_commands(main)
|
|
@@ -46,17 +46,17 @@ def resolve_agent_home(agent: str) -> Path:
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def apply_agent_override(agent: str) -> None:
|
|
49
|
-
"""
|
|
49
|
+
"""Set the active agent name when --agent is specified.
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
Only mutates SKCAPSTONE_AGENT so that agent_home() resolves to
|
|
52
|
+
the correct per-agent directory. Does NOT mutate AGENT_HOME
|
|
53
|
+
(the shared root) — that would break agent_home() (double
|
|
54
|
+
nesting) and shared_home() (wrong path).
|
|
53
55
|
|
|
54
56
|
Args:
|
|
55
57
|
agent: Agent name from the --agent CLI option or env var.
|
|
56
58
|
"""
|
|
57
59
|
if agent:
|
|
58
|
-
new_home = str(Path(SHARED_ROOT) / "agents" / agent)
|
|
59
|
-
_pkg.AGENT_HOME = new_home
|
|
60
60
|
_pkg.SKCAPSTONE_AGENT = agent
|
|
61
61
|
os.environ["SKCAPSTONE_AGENT"] = agent
|
|
62
62
|
|
|
@@ -48,20 +48,34 @@ def register_card_commands(main: click.Group) -> None:
|
|
|
48
48
|
except FileNotFoundError:
|
|
49
49
|
runtime = get_runtime(home_path)
|
|
50
50
|
m = runtime.manifest
|
|
51
|
+
# Try to load public key from capauth
|
|
52
|
+
pub_key = ""
|
|
53
|
+
pub_path = Path(capauth_home).expanduser() / "identity" / "public.asc"
|
|
54
|
+
if pub_path.exists():
|
|
55
|
+
pub_key = pub_path.read_text(encoding="utf-8")
|
|
51
56
|
agent_card = AgentCard.generate(
|
|
52
57
|
name=m.name, fingerprint=m.identity.fingerprint or "unknown",
|
|
53
|
-
public_key=
|
|
58
|
+
public_key=pub_key, entity_type="ai",
|
|
54
59
|
)
|
|
55
60
|
|
|
56
61
|
if motto:
|
|
57
62
|
agent_card.motto = motto
|
|
58
63
|
|
|
59
64
|
if do_sign:
|
|
60
|
-
if not passphrase:
|
|
61
|
-
passphrase = click.prompt("PGP passphrase", hide_input=True)
|
|
62
65
|
capauth_path = Path(capauth_home).expanduser()
|
|
63
66
|
priv_path = capauth_path / "identity" / "private.asc"
|
|
64
67
|
if priv_path.exists():
|
|
68
|
+
# Check if key is passphrase-protected before prompting
|
|
69
|
+
if not passphrase:
|
|
70
|
+
try:
|
|
71
|
+
import pgpy
|
|
72
|
+
key, _ = pgpy.PGPKey.from_file(str(priv_path))
|
|
73
|
+
if key.is_protected:
|
|
74
|
+
passphrase = click.prompt("PGP passphrase", hide_input=True)
|
|
75
|
+
else:
|
|
76
|
+
passphrase = ""
|
|
77
|
+
except Exception:
|
|
78
|
+
passphrase = click.prompt("PGP passphrase", hide_input=True)
|
|
65
79
|
agent_card.sign(priv_path.read_text(encoding="utf-8"), passphrase)
|
|
66
80
|
console.print("[green]Card signed.[/]")
|
|
67
81
|
else:
|
|
@@ -74,15 +88,32 @@ def register_card_commands(main: click.Group) -> None:
|
|
|
74
88
|
console.print(f" [dim]Saved to: {out_path}[/]\n")
|
|
75
89
|
|
|
76
90
|
@card.command("show")
|
|
77
|
-
@click.argument("filepath", default=
|
|
91
|
+
@click.argument("filepath", default=None, required=False)
|
|
78
92
|
def card_show(filepath):
|
|
79
|
-
"""Display an agent card.
|
|
93
|
+
"""Display an agent card.
|
|
94
|
+
|
|
95
|
+
If no filepath is given, looks in the agent home directory first,
|
|
96
|
+
then falls back to ~/.skcapstone/agent-card.json.
|
|
97
|
+
"""
|
|
80
98
|
from ..agent_card import AgentCard
|
|
99
|
+
from .. import agent_home, AGENT_HOME
|
|
100
|
+
|
|
101
|
+
if filepath is None:
|
|
102
|
+
# Try agent-scoped path first, then shared root
|
|
103
|
+
candidates = [
|
|
104
|
+
Path(agent_home()) / "agent-card.json",
|
|
105
|
+
Path(AGENT_HOME).expanduser() / "agent-card.json",
|
|
106
|
+
]
|
|
107
|
+
filepath = next(
|
|
108
|
+
(str(c) for c in candidates if c.exists()),
|
|
109
|
+
str(candidates[0]), # default for error message
|
|
110
|
+
)
|
|
81
111
|
|
|
82
112
|
try:
|
|
83
113
|
agent_card = AgentCard.load(filepath)
|
|
84
114
|
except FileNotFoundError:
|
|
85
115
|
console.print(f"[red]Card not found: {filepath}[/]")
|
|
116
|
+
console.print("[dim]Generate one with: skcapstone card generate[/]")
|
|
86
117
|
raise SystemExit(1)
|
|
87
118
|
|
|
88
119
|
verified = AgentCard.verify_signature(agent_card)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Config commands: validate."""
|
|
1
|
+
"""Config commands: show, validate."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -18,6 +18,58 @@ def register_config_commands(main: click.Group) -> None:
|
|
|
18
18
|
def config():
|
|
19
19
|
"""Config management — validate and inspect agent configuration."""
|
|
20
20
|
|
|
21
|
+
@config.command("show")
|
|
22
|
+
@click.option(
|
|
23
|
+
"--home", default=AGENT_HOME, type=click.Path(),
|
|
24
|
+
help="Agent home directory.",
|
|
25
|
+
)
|
|
26
|
+
@click.option("--json-out", is_flag=True, help="Output as machine-readable JSON.")
|
|
27
|
+
def show(home: str, json_out: bool) -> None:
|
|
28
|
+
"""Show current agent configuration.
|
|
29
|
+
|
|
30
|
+
Displays the contents of config.yaml, consciousness.yaml, and
|
|
31
|
+
model_profiles.yaml from the agent home directory.
|
|
32
|
+
"""
|
|
33
|
+
import yaml
|
|
34
|
+
|
|
35
|
+
home_path = Path(home).expanduser()
|
|
36
|
+
config_dir = home_path / "config"
|
|
37
|
+
|
|
38
|
+
if not config_dir.exists():
|
|
39
|
+
console.print(f"[red]Config directory not found: {config_dir}[/]")
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
config_files = ["config.yaml", "consciousness.yaml", "model_profiles.yaml"]
|
|
43
|
+
all_data: dict = {}
|
|
44
|
+
|
|
45
|
+
for fname in config_files:
|
|
46
|
+
fpath = config_dir / fname
|
|
47
|
+
if fpath.exists():
|
|
48
|
+
try:
|
|
49
|
+
data = yaml.safe_load(fpath.read_text(encoding="utf-8"))
|
|
50
|
+
all_data[fname] = data
|
|
51
|
+
except Exception as exc:
|
|
52
|
+
all_data[fname] = {"error": str(exc)}
|
|
53
|
+
else:
|
|
54
|
+
all_data[fname] = None
|
|
55
|
+
|
|
56
|
+
if json_out:
|
|
57
|
+
click.echo(json.dumps(all_data, indent=2, default=str))
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
for fname, data in all_data.items():
|
|
61
|
+
if data is None:
|
|
62
|
+
console.print(f" [dim]{fname}[/] [yellow]not found[/]")
|
|
63
|
+
elif "error" in data:
|
|
64
|
+
console.print(f" [dim]{fname}[/] [red]{data['error']}[/]")
|
|
65
|
+
else:
|
|
66
|
+
console.print(f"\n [bold cyan]{fname}[/]")
|
|
67
|
+
console.print(f" [dim]{config_dir / fname}[/]")
|
|
68
|
+
formatted = yaml.dump(data, default_flow_style=False, indent=2)
|
|
69
|
+
for line in formatted.strip().split("\n"):
|
|
70
|
+
console.print(f" {line}")
|
|
71
|
+
console.print()
|
|
72
|
+
|
|
21
73
|
@config.command("validate")
|
|
22
74
|
@click.option(
|
|
23
75
|
"--home", default=AGENT_HOME, type=click.Path(),
|