@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.
Files changed (47) hide show
  1. package/README.md +61 -0
  2. package/docs/CUSTOM_AGENT.md +184 -0
  3. package/docs/GETTING_STARTED.md +3 -0
  4. package/openclaw-plugin/src/index.ts +75 -4
  5. package/package.json +1 -1
  6. package/pyproject.toml +1 -1
  7. package/scripts/archive-sessions.sh +72 -0
  8. package/scripts/install.ps1 +2 -1
  9. package/scripts/install.sh +2 -1
  10. package/scripts/nvidia-proxy.mjs +727 -0
  11. package/scripts/telegram-catchup-all.sh +136 -0
  12. package/src/skcapstone/__init__.py +70 -1
  13. package/src/skcapstone/agent_card.py +4 -1
  14. package/src/skcapstone/blueprint_registry.py +78 -0
  15. package/src/skcapstone/blueprints/builtins/itil-operations.yaml +40 -0
  16. package/src/skcapstone/cli/__init__.py +2 -0
  17. package/src/skcapstone/cli/_common.py +5 -5
  18. package/src/skcapstone/cli/card.py +36 -5
  19. package/src/skcapstone/cli/config_cmd.py +53 -1
  20. package/src/skcapstone/cli/itil.py +434 -0
  21. package/src/skcapstone/cli/peer.py +3 -1
  22. package/src/skcapstone/cli/peers_dir.py +3 -1
  23. package/src/skcapstone/cli/preflight_cmd.py +4 -0
  24. package/src/skcapstone/cli/skills_cmd.py +120 -24
  25. package/src/skcapstone/cli/soul.py +47 -24
  26. package/src/skcapstone/cli/status.py +17 -11
  27. package/src/skcapstone/cli/usage_cmd.py +7 -2
  28. package/src/skcapstone/consciousness_config.py +27 -0
  29. package/src/skcapstone/coordination.py +1 -0
  30. package/src/skcapstone/daemon.py +28 -9
  31. package/src/skcapstone/defaults/lumina/manifest.json +1 -1
  32. package/src/skcapstone/doctor.py +115 -0
  33. package/src/skcapstone/dreaming.py +761 -0
  34. package/src/skcapstone/itil.py +1104 -0
  35. package/src/skcapstone/mcp_server.py +258 -0
  36. package/src/skcapstone/mcp_tools/__init__.py +2 -0
  37. package/src/skcapstone/mcp_tools/gtd_tools.py +1 -1
  38. package/src/skcapstone/mcp_tools/itil_tools.py +657 -0
  39. package/src/skcapstone/mcp_tools/notification_tools.py +12 -11
  40. package/src/skcapstone/notifications.py +40 -27
  41. package/src/skcapstone/onboard.py +46 -0
  42. package/src/skcapstone/pillars/sync.py +11 -4
  43. package/src/skcapstone/register.py +8 -0
  44. package/src/skcapstone/scheduled_tasks.py +107 -0
  45. package/src/skcapstone/service_health.py +81 -2
  46. package/src/skcapstone/soul.py +19 -0
  47. package/systemd/skcapstone.service +5 -6
@@ -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()
@@ -195,19 +195,25 @@ def register_status_commands(main: click.Group) -> None:
195
195
  """Register all status/overview commands on the main CLI group."""
196
196
 
197
197
  @main.command()
198
- @click.option("--home", default=AGENT_HOME, help="Agent home directory.", type=click.Path())
199
- def status(home: str):
198
+ @click.option("--home", default=None, help="Agent home directory.", type=click.Path())
199
+ @click.option("--agent", default=None, help="Agent name (e.g. opus, lumina).")
200
+ def status(home: Optional[str], agent: Optional[str]):
200
201
  """Show the sovereign agent's current state."""
201
- home_path = Path(home).expanduser()
202
-
203
- if not home_path.exists():
204
- console.print(
205
- "[bold red]No agent found.[/] "
206
- "Run [bold]skcapstone init --name \"YourAgent\"[/] first."
207
- )
208
- sys.exit(1)
202
+ from .. import SKCAPSTONE_AGENT as default_agent
209
203
 
210
- runtime = get_runtime(home_path)
204
+ if home:
205
+ home_path = Path(home).expanduser()
206
+ if not home_path.exists():
207
+ console.print(
208
+ "[bold red]No agent found.[/] "
209
+ "Run [bold]skcapstone init --name \"YourAgent\"[/] first."
210
+ )
211
+ sys.exit(1)
212
+ runtime = get_runtime(home=home_path)
213
+ else:
214
+ agent_name = agent or default_agent
215
+ runtime = get_runtime(agent_name=agent_name)
216
+ home_path = runtime.home
211
217
  m = runtime.manifest
212
218
 
213
219
  console.print()
@@ -14,13 +14,18 @@ from ._common import AGENT_HOME, console
14
14
  def register_usage_commands(main: click.Group) -> None:
15
15
  """Register the ``skcapstone usage`` command group."""
16
16
 
17
- @main.group("usage")
18
- def usage_group():
17
+ @main.group("usage", invoke_without_command=True)
18
+ @click.pass_context
19
+ def usage_group(ctx):
19
20
  """Show LLM token usage and cost estimates.
20
21
 
21
22
  Tracks input/output tokens per model per day.
22
23
  Data is stored in ~/.skcapstone/usage/tokens-{date}.json.
24
+
25
+ When called without a subcommand, shows today's usage.
23
26
  """
27
+ if ctx.invoked_subcommand is None:
28
+ ctx.invoke(today_cmd)
24
29
 
25
30
  @usage_group.command("daily")
26
31
  @click.option("--home", default=AGENT_HOME, type=click.Path(), help="Agent home directory.")
@@ -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)."""
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lumina",
3
3
  "display_name": "Lumina",
4
- "version": "0.2.5",
4
+ "version": "0.4.2",
5
5
  "description": "Default sovereign agent — warm, curious, sovereignty-first",
6
6
  "author": "smilinTux",
7
7
  "license": "GPL-3.0-or-later",
@@ -13,6 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  import importlib
15
15
  import json
16
+ import os
16
17
  import shutil
17
18
  import subprocess
18
19
  from dataclasses import dataclass, field
@@ -618,6 +619,120 @@ def _check_versions() -> list[Check]:
618
619
  return checks
619
620
 
620
621
 
622
+ @dataclass
623
+ class FixResult:
624
+ """Result of attempting to auto-fix a failing check.
625
+
626
+ Attributes:
627
+ check_name: Name of the check that was fixed.
628
+ success: Whether the fix succeeded.
629
+ action: Description of what was done.
630
+ error: Error message if the fix failed.
631
+ """
632
+
633
+ check_name: str
634
+ success: bool
635
+ action: str = ""
636
+ error: str = ""
637
+
638
+
639
+ def run_fixes(report: DiagnosticReport, home: Path) -> list[FixResult]:
640
+ """Attempt to auto-fix failing checks by creating missing directories and files.
641
+
642
+ Args:
643
+ report: Diagnostic report with failing checks.
644
+ home: Agent home directory.
645
+
646
+ Returns:
647
+ List of FixResult for each attempted fix.
648
+ """
649
+ results: list[FixResult] = []
650
+
651
+ for check in report.checks:
652
+ if check.passed:
653
+ continue
654
+
655
+ # Fix missing directories
656
+ if check.name.startswith("home:") and check.name != "home:exists" and check.name != "home:manifest":
657
+ dirname = check.name.split(":", 1)[1]
658
+ dirpath = home / dirname
659
+ try:
660
+ dirpath.mkdir(parents=True, exist_ok=True)
661
+ results.append(FixResult(
662
+ check_name=check.name,
663
+ success=True,
664
+ action=f"Created directory {dirpath}",
665
+ ))
666
+ except OSError as exc:
667
+ results.append(FixResult(
668
+ check_name=check.name,
669
+ success=False,
670
+ error=str(exc),
671
+ ))
672
+
673
+ # Fix missing manifest
674
+ elif check.name == "home:manifest":
675
+ manifest_path = home / "manifest.json"
676
+ try:
677
+ data = {
678
+ "name": os.environ.get("SKCAPSTONE_AGENT", "sovereign"),
679
+ "version": "0.0.0",
680
+ "created_at": "",
681
+ "connectors": [],
682
+ }
683
+ manifest_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
684
+ results.append(FixResult(
685
+ check_name=check.name,
686
+ success=True,
687
+ action=f"Created default manifest at {manifest_path}",
688
+ ))
689
+ except OSError as exc:
690
+ results.append(FixResult(
691
+ check_name=check.name,
692
+ success=False,
693
+ error=str(exc),
694
+ ))
695
+
696
+ # Fix missing memory store
697
+ elif check.name == "memory:store":
698
+ agent_name = os.environ.get("SKCAPSTONE_AGENT", "lumina")
699
+ memory_dir = home / "agents" / agent_name / "memory"
700
+ try:
701
+ for layer in ("short-term", "mid-term", "long-term"):
702
+ (memory_dir / layer).mkdir(parents=True, exist_ok=True)
703
+ results.append(FixResult(
704
+ check_name=check.name,
705
+ success=True,
706
+ action=f"Created memory directories at {memory_dir}",
707
+ ))
708
+ except OSError as exc:
709
+ results.append(FixResult(
710
+ check_name=check.name,
711
+ success=False,
712
+ error=str(exc),
713
+ ))
714
+
715
+ # Fix missing sync directory
716
+ elif check.name == "sync:dir":
717
+ sync_dir = home / "sync"
718
+ try:
719
+ for subdir in ("outbox", "inbox", "archive"):
720
+ (sync_dir / subdir).mkdir(parents=True, exist_ok=True)
721
+ results.append(FixResult(
722
+ check_name=check.name,
723
+ success=True,
724
+ action=f"Created sync directories at {sync_dir}",
725
+ ))
726
+ except OSError as exc:
727
+ results.append(FixResult(
728
+ check_name=check.name,
729
+ success=False,
730
+ error=str(exc),
731
+ ))
732
+
733
+ return results
734
+
735
+
621
736
  def _get_tool_version(tool: str) -> Optional[str]:
622
737
  """Try to get a tool's version string.
623
738