@smilintux/skcapstone 0.4.5 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/skcapstone/blueprint_registry.py +78 -0
- package/src/skcapstone/cli/__init__.py +2 -0
- package/src/skcapstone/cli/itil.py +434 -0
- package/src/skcapstone/cli/skills_cmd.py +90 -26
- package/src/skcapstone/cli/soul.py +47 -24
- 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/dreaming.py +761 -0
- package/src/skcapstone/itil.py +1104 -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/scheduled_tasks.py +107 -0
- package/src/skcapstone/service_health.py +81 -2
- package/src/skcapstone/soul.py +19 -0
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import logging
|
|
6
7
|
import sys
|
|
8
|
+
import urllib.request
|
|
9
|
+
from typing import Optional
|
|
7
10
|
|
|
8
11
|
import click
|
|
12
|
+
import yaml
|
|
9
13
|
|
|
10
14
|
from ._common import AGENT_HOME, console
|
|
11
15
|
from ..registry_client import get_registry_client
|
|
@@ -13,18 +17,65 @@ from ..registry_client import get_registry_client
|
|
|
13
17
|
from rich.panel import Panel
|
|
14
18
|
from rich.table import Table
|
|
15
19
|
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Raw catalog.yaml from the skskills GitHub repo (always fresh)
|
|
23
|
+
_GITHUB_CATALOG_URL = (
|
|
24
|
+
"https://raw.githubusercontent.com/smilinTux/skskills/main/catalog.yaml"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _fetch_github_catalog(query: str = "") -> Optional[list[dict]]:
|
|
29
|
+
"""Fetch catalog.yaml from the skskills GitHub repo.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
List of skill entry dicts, or None on failure.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
req = urllib.request.Request(_GITHUB_CATALOG_URL, headers={"User-Agent": "skcapstone"})
|
|
36
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
37
|
+
raw = yaml.safe_load(resp.read().decode("utf-8"))
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
logger.debug("GitHub catalog fetch failed: %s", exc)
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
entries = []
|
|
43
|
+
q = query.lower()
|
|
44
|
+
for item in raw.get("skills", []):
|
|
45
|
+
name = item.get("name", "")
|
|
46
|
+
desc = item.get("description", "").strip()
|
|
47
|
+
tags = item.get("tags", [])
|
|
48
|
+
|
|
49
|
+
if q and not (
|
|
50
|
+
q in name.lower()
|
|
51
|
+
or q in desc.lower()
|
|
52
|
+
or any(q in t.lower() for t in tags)
|
|
53
|
+
):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
entries.append({
|
|
57
|
+
"name": name,
|
|
58
|
+
"description": desc,
|
|
59
|
+
"tags": tags,
|
|
60
|
+
"category": item.get("category", ""),
|
|
61
|
+
"pip": item.get("pip", ""),
|
|
62
|
+
"git": item.get("git", ""),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return entries
|
|
66
|
+
|
|
16
67
|
|
|
17
68
|
def register_skills_commands(main: click.Group) -> None:
|
|
18
69
|
"""Register the skills command group."""
|
|
19
70
|
|
|
20
71
|
@main.group()
|
|
21
72
|
def skills():
|
|
22
|
-
"""
|
|
73
|
+
"""Skills registry — discover and install agent skills.
|
|
23
74
|
|
|
24
|
-
|
|
25
|
-
|
|
75
|
+
Fetches the latest skill catalog from GitHub. Falls back to the
|
|
76
|
+
locally installed catalog if offline.
|
|
26
77
|
|
|
27
|
-
Set SKSKILLS_REGISTRY_URL to override
|
|
78
|
+
Set SKSKILLS_REGISTRY_URL to override with a custom registry server.
|
|
28
79
|
"""
|
|
29
80
|
|
|
30
81
|
@skills.command("list")
|
|
@@ -36,11 +87,12 @@ def register_skills_commands(main: click.Group) -> None:
|
|
|
36
87
|
help="Override the skills registry URL.",
|
|
37
88
|
)
|
|
38
89
|
@click.option("--json", "json_out", is_flag=True, help="Output raw JSON.")
|
|
39
|
-
|
|
40
|
-
|
|
90
|
+
@click.option("--offline", is_flag=True, help="Use local catalog only (no network).")
|
|
91
|
+
def skills_list(query: str, registry: str | None, json_out: bool, offline: bool) -> None:
|
|
92
|
+
"""List skills available in the catalog.
|
|
41
93
|
|
|
42
|
-
|
|
43
|
-
|
|
94
|
+
Pulls the latest catalog from the skskills GitHub repo.
|
|
95
|
+
Falls back to local catalog if offline or fetch fails.
|
|
44
96
|
|
|
45
97
|
Examples:
|
|
46
98
|
|
|
@@ -49,19 +101,28 @@ def register_skills_commands(main: click.Group) -> None:
|
|
|
49
101
|
skcapstone skills list --query syncthing
|
|
50
102
|
|
|
51
103
|
skcapstone skills list --query identity --json
|
|
104
|
+
|
|
105
|
+
skcapstone skills list --offline
|
|
52
106
|
"""
|
|
53
|
-
# Try remote registry first, fall back to local catalog
|
|
54
107
|
skill_entries = None
|
|
55
|
-
source = "
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
108
|
+
source = "github"
|
|
109
|
+
|
|
110
|
+
# 1. Try custom registry server if configured
|
|
111
|
+
if registry:
|
|
112
|
+
client = get_registry_client(registry)
|
|
113
|
+
if client is not None:
|
|
114
|
+
try:
|
|
115
|
+
skill_entries = client.search(query) if query else client.list_skills()
|
|
116
|
+
source = "remote"
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
# 2. Try GitHub raw catalog (always fresh, no server needed)
|
|
121
|
+
if skill_entries is None and not offline:
|
|
122
|
+
skill_entries = _fetch_github_catalog(query)
|
|
123
|
+
source = "github"
|
|
124
|
+
|
|
125
|
+
# 3. Fall back to local catalog (bundled with skskills package)
|
|
65
126
|
if skill_entries is None:
|
|
66
127
|
try:
|
|
67
128
|
from skskills.catalog import SkillCatalog
|
|
@@ -74,7 +135,6 @@ def register_skills_commands(main: click.Group) -> None:
|
|
|
74
135
|
skill_entries = [
|
|
75
136
|
{
|
|
76
137
|
"name": e.name,
|
|
77
|
-
"version": "",
|
|
78
138
|
"description": e.description,
|
|
79
139
|
"tags": e.tags,
|
|
80
140
|
"category": e.category,
|
|
@@ -83,10 +143,10 @@ def register_skills_commands(main: click.Group) -> None:
|
|
|
83
143
|
}
|
|
84
144
|
for e in entries
|
|
85
145
|
]
|
|
86
|
-
source = "
|
|
146
|
+
source = "local"
|
|
87
147
|
except ImportError:
|
|
88
148
|
console.print(
|
|
89
|
-
"[bold red]skskills not installed.[/] "
|
|
149
|
+
"[bold red]skskills not installed and GitHub unreachable.[/] "
|
|
90
150
|
"Run: pip install skskills"
|
|
91
151
|
)
|
|
92
152
|
sys.exit(1)
|
|
@@ -103,11 +163,15 @@ def register_skills_commands(main: click.Group) -> None:
|
|
|
103
163
|
console.print(f"\n [dim]No skills found{suffix}.[/]\n")
|
|
104
164
|
return
|
|
105
165
|
|
|
166
|
+
source_labels = {
|
|
167
|
+
"github": "",
|
|
168
|
+
"remote": " [dim](registry)[/]",
|
|
169
|
+
"local": " [dim](local — offline)[/]",
|
|
170
|
+
}
|
|
106
171
|
label = f"[bold]{len(skill_entries)}[/] skill(s)"
|
|
107
172
|
if query:
|
|
108
173
|
label += f" matching [cyan]'{query}'[/]"
|
|
109
|
-
|
|
110
|
-
label += " [dim](local catalog)[/]"
|
|
174
|
+
label += source_labels.get(source, "")
|
|
111
175
|
|
|
112
176
|
table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
|
|
113
177
|
table.add_column("Name", style="cyan")
|
|
@@ -118,13 +182,13 @@ def register_skills_commands(main: click.Group) -> None:
|
|
|
118
182
|
for s in skill_entries:
|
|
119
183
|
table.add_row(
|
|
120
184
|
s.get("name", ""),
|
|
121
|
-
s.get("category",
|
|
185
|
+
s.get("category", ""),
|
|
122
186
|
s.get("description", ""),
|
|
123
187
|
", ".join(s.get("tags", [])),
|
|
124
188
|
)
|
|
125
189
|
|
|
126
190
|
console.print()
|
|
127
|
-
console.print(Panel(label, title="Skills
|
|
191
|
+
console.print(Panel(label, title="Skills Catalog", border_style="bright_blue"))
|
|
128
192
|
console.print(table)
|
|
129
193
|
console.print()
|
|
130
194
|
|
|
@@ -517,22 +517,36 @@ def register_soul_commands(main: click.Group) -> None:
|
|
|
517
517
|
@soul_registry.command("list")
|
|
518
518
|
@click.option("--url", default=None, help="Registry API base URL.")
|
|
519
519
|
def registry_list(url):
|
|
520
|
-
"""List all blueprints in the remote registry.
|
|
521
|
-
from ..blueprint_registry import BlueprintRegistryClient, BlueprintRegistryError
|
|
520
|
+
"""List all blueprints in the remote registry.
|
|
522
521
|
|
|
523
|
-
|
|
522
|
+
Pulls from the soul-blueprints GitHub repo. Falls back to
|
|
523
|
+
the souls.skworld.io API if --url is set.
|
|
524
|
+
"""
|
|
525
|
+
from ..blueprint_registry import (
|
|
526
|
+
BlueprintRegistryClient,
|
|
527
|
+
BlueprintRegistryError,
|
|
528
|
+
_fetch_github_blueprints,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
blueprints = None
|
|
532
|
+
source = "github"
|
|
533
|
+
|
|
534
|
+
# If custom URL, try the API server first
|
|
524
535
|
if url:
|
|
525
|
-
|
|
526
|
-
|
|
536
|
+
try:
|
|
537
|
+
client = BlueprintRegistryClient(base_url=url)
|
|
538
|
+
blueprints = client.list_blueprints()
|
|
539
|
+
source = "registry"
|
|
540
|
+
except BlueprintRegistryError:
|
|
541
|
+
pass
|
|
527
542
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
sys.exit(1)
|
|
543
|
+
# Default: pull from GitHub repo
|
|
544
|
+
if blueprints is None:
|
|
545
|
+
blueprints = _fetch_github_blueprints()
|
|
546
|
+
source = "github"
|
|
533
547
|
|
|
534
548
|
if not blueprints:
|
|
535
|
-
console.print("\n [dim]No blueprints found
|
|
549
|
+
console.print("\n [dim]No blueprints found.[/]\n")
|
|
536
550
|
return
|
|
537
551
|
|
|
538
552
|
table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
|
|
@@ -547,27 +561,38 @@ def register_soul_commands(main: click.Group) -> None:
|
|
|
547
561
|
bp.get("category", ""),
|
|
548
562
|
)
|
|
549
563
|
|
|
564
|
+
source_label = "" if source == "github" else " [dim](registry)[/]"
|
|
550
565
|
console.print()
|
|
551
566
|
console.print(table)
|
|
552
|
-
console.print(f"\n [dim]{len(blueprints)} blueprint(s)
|
|
567
|
+
console.print(f"\n [dim]{len(blueprints)} blueprint(s){source_label}[/]\n")
|
|
553
568
|
|
|
554
569
|
@soul_registry.command("search")
|
|
555
570
|
@click.argument("query")
|
|
556
571
|
@click.option("--url", default=None, help="Registry API base URL.")
|
|
557
572
|
def registry_search(query, url):
|
|
558
|
-
"""Search the remote registry for blueprints.
|
|
559
|
-
from ..blueprint_registry import BlueprintRegistryClient, BlueprintRegistryError
|
|
573
|
+
"""Search the remote registry for blueprints.
|
|
560
574
|
|
|
561
|
-
|
|
575
|
+
Searches the soul-blueprints GitHub repo by name and category.
|
|
576
|
+
"""
|
|
577
|
+
from ..blueprint_registry import (
|
|
578
|
+
BlueprintRegistryClient,
|
|
579
|
+
BlueprintRegistryError,
|
|
580
|
+
_fetch_github_blueprints,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
results = None
|
|
584
|
+
|
|
585
|
+
# If custom URL, try the API server first
|
|
562
586
|
if url:
|
|
563
|
-
|
|
564
|
-
|
|
587
|
+
try:
|
|
588
|
+
client = BlueprintRegistryClient(base_url=url)
|
|
589
|
+
results = client.search_blueprints(query)
|
|
590
|
+
except BlueprintRegistryError:
|
|
591
|
+
pass
|
|
565
592
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
console.print(f"\n [red]Registry error:[/] {e}\n")
|
|
570
|
-
sys.exit(1)
|
|
593
|
+
# Default: search GitHub repo
|
|
594
|
+
if results is None:
|
|
595
|
+
results = _fetch_github_blueprints(query)
|
|
571
596
|
|
|
572
597
|
if not results:
|
|
573
598
|
console.print(f"\n [dim]No blueprints matching '{query}'.[/]\n")
|
|
@@ -577,14 +602,12 @@ def register_soul_commands(main: click.Group) -> None:
|
|
|
577
602
|
table.add_column("Name", style="cyan", no_wrap=True)
|
|
578
603
|
table.add_column("Display Name", style="bold")
|
|
579
604
|
table.add_column("Category", style="yellow")
|
|
580
|
-
table.add_column("Vibe", style="dim")
|
|
581
605
|
|
|
582
606
|
for bp in results:
|
|
583
607
|
table.add_row(
|
|
584
608
|
bp.get("name", "?"),
|
|
585
609
|
bp.get("display_name", ""),
|
|
586
610
|
bp.get("category", ""),
|
|
587
|
-
(bp.get("vibe", "") or "")[:60],
|
|
588
611
|
)
|
|
589
612
|
|
|
590
613
|
console.print()
|
|
@@ -117,3 +117,30 @@ def write_default_config(home: Path) -> Path:
|
|
|
117
117
|
config_path.write_text(header + content, encoding="utf-8")
|
|
118
118
|
logger.info("Wrote default consciousness config to %s", config_path)
|
|
119
119
|
return config_path
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def load_dreaming_config(
|
|
123
|
+
home: Path,
|
|
124
|
+
config_path: Optional[Path] = None,
|
|
125
|
+
):
|
|
126
|
+
"""Load dreaming config from the consciousness.yaml ``dreaming:`` section.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
home: Agent home directory.
|
|
130
|
+
config_path: Explicit path to config file (overrides default).
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
DreamingConfig (defaults if section is missing or unparseable).
|
|
134
|
+
"""
|
|
135
|
+
from .dreaming import DreamingConfig
|
|
136
|
+
|
|
137
|
+
yaml_path = config_path or (home / "config" / CONFIG_FILENAME)
|
|
138
|
+
if not yaml_path.exists():
|
|
139
|
+
return DreamingConfig()
|
|
140
|
+
try:
|
|
141
|
+
raw = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
|
|
142
|
+
if raw and isinstance(raw, dict) and "dreaming" in raw:
|
|
143
|
+
return DreamingConfig.model_validate(raw["dreaming"])
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
logger.warning("Failed to parse dreaming config: %s", exc)
|
|
146
|
+
return DreamingConfig()
|
|
@@ -123,6 +123,7 @@ class AgentFile(BaseModel):
|
|
|
123
123
|
claimed_tasks: list[str] = Field(default_factory=list)
|
|
124
124
|
completed_tasks: list[str] = Field(default_factory=list)
|
|
125
125
|
capabilities: list[str] = Field(default_factory=list)
|
|
126
|
+
itil_claims: list[str] = Field(default_factory=list)
|
|
126
127
|
notes: str = ""
|
|
127
128
|
|
|
128
129
|
|
package/src/skcapstone/daemon.py
CHANGED
|
@@ -1145,16 +1145,35 @@ class DaemonService:
|
|
|
1145
1145
|
logger.debug("Auto-journal write failed: %s", exc)
|
|
1146
1146
|
|
|
1147
1147
|
try:
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
self.config.home,
|
|
1151
|
-
f"Received message from {sender}: {preview}",
|
|
1152
|
-
tags=["skcomm-received"],
|
|
1153
|
-
source="daemon",
|
|
1154
|
-
)
|
|
1155
|
-
logger.debug("Memory stored for incoming message from %s", sender)
|
|
1148
|
+
self._store_skcomm_receipt(sender, preview)
|
|
1149
|
+
logger.debug("SKComm receipt stored for incoming message from %s", sender)
|
|
1156
1150
|
except Exception as exc:
|
|
1157
|
-
logger.debug("
|
|
1151
|
+
logger.debug("SKComm receipt store failed: %s", exc)
|
|
1152
|
+
|
|
1153
|
+
def _store_skcomm_receipt(self, sender: str, preview: str) -> None:
|
|
1154
|
+
"""Write a skcomm receipt to the skcomm/received/ directory.
|
|
1155
|
+
|
|
1156
|
+
These are transport bookkeeping, NOT persistent memories, so they
|
|
1157
|
+
go to ``~/.skcapstone/agents/{agent}/skcomm/received/`` instead of
|
|
1158
|
+
polluting the memory/ tree that skmemory indexes.
|
|
1159
|
+
"""
|
|
1160
|
+
import json
|
|
1161
|
+
import uuid
|
|
1162
|
+
from datetime import datetime, timezone
|
|
1163
|
+
|
|
1164
|
+
agent_name = os.environ.get("SKCAPSTONE_AGENT", "lumina")
|
|
1165
|
+
recv_dir = self.config.home / "agents" / agent_name / "skcomm" / "received"
|
|
1166
|
+
recv_dir.mkdir(parents=True, exist_ok=True)
|
|
1167
|
+
|
|
1168
|
+
receipt_id = uuid.uuid4().hex[:12]
|
|
1169
|
+
receipt = {
|
|
1170
|
+
"id": receipt_id,
|
|
1171
|
+
"sender": sender,
|
|
1172
|
+
"preview": preview,
|
|
1173
|
+
"received_at": datetime.now(timezone.utc).isoformat(),
|
|
1174
|
+
}
|
|
1175
|
+
path = recv_dir / f"{receipt_id}.json"
|
|
1176
|
+
path.write_text(json.dumps(receipt, indent=2), encoding="utf-8")
|
|
1158
1177
|
|
|
1159
1178
|
def _healing_loop(self) -> None:
|
|
1160
1179
|
"""Periodically run self-healing diagnostics (every 5 min)."""
|