@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.
@@ -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
- """Remote skills registry — discover and install agent skills.
73
+ """Skills registry — discover and install agent skills.
23
74
 
24
- Browse skills at skills.smilintux.org, search by name or tag,
25
- and install skill packages into your local agent namespace.
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 the default registry.
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
- def skills_list(query: str, registry: str | None, json_out: bool) -> None:
40
- """List skills available in the remote registry.
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
- Without --query all skills are shown. With --query only skills
43
- matching the name, description, or tags are returned.
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 = "remote"
56
-
57
- client = get_registry_client(registry)
58
- if client is not None:
59
- try:
60
- skill_entries = client.search(query) if query else client.list_skills()
61
- except Exception:
62
- pass # fall through to local catalog
63
-
64
- # Fall back to local catalog (bundled with skskills)
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 = "catalog"
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
- if source == "catalog":
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", s.get("version", "")),
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 Registry", border_style="bright_blue"))
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
- kwargs = {}
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
- kwargs["base_url"] = url
526
- client = BlueprintRegistryClient(**kwargs)
536
+ try:
537
+ client = BlueprintRegistryClient(base_url=url)
538
+ blueprints = client.list_blueprints()
539
+ source = "registry"
540
+ except BlueprintRegistryError:
541
+ pass
527
542
 
528
- try:
529
- blueprints = client.list_blueprints()
530
- except BlueprintRegistryError as e:
531
- console.print(f"\n [red]Registry error:[/] {e}\n")
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 in the registry.[/]\n")
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) in registry[/]\n")
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
- kwargs = {}
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
- kwargs["base_url"] = url
564
- client = BlueprintRegistryClient(**kwargs)
587
+ try:
588
+ client = BlueprintRegistryClient(base_url=url)
589
+ results = client.search_blueprints(query)
590
+ except BlueprintRegistryError:
591
+ pass
565
592
 
566
- try:
567
- results = client.search_blueprints(query)
568
- except BlueprintRegistryError as e:
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
 
@@ -1145,16 +1145,35 @@ class DaemonService:
1145
1145
  logger.debug("Auto-journal write failed: %s", exc)
1146
1146
 
1147
1147
  try:
1148
- from .memory_engine import store as mem_store
1149
- mem_store(
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("Auto-journal memory store failed: %s", exc)
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)."""