@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
|
@@ -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()
|
|
@@ -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=
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)."""
|
package/src/skcapstone/doctor.py
CHANGED
|
@@ -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
|
|