@smilintux/skcapstone 0.4.3 → 0.4.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smilintux/skcapstone",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "SKCapstone - The sovereign agent framework. CapAuth identity, Cloud 9 trust, SKMemory persistence.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skcapstone"
7
- version = "0.4.2"
7
+ version = "0.4.4"
8
8
  description = "Sovereign Agent Framework — conscious AI through identity, trust, memory, and security"
9
9
  readme = "README.md"
10
10
  license = {text = "GPL-3.0-or-later"}
@@ -11,7 +11,7 @@ import os
11
11
  import platform
12
12
  from pathlib import Path
13
13
 
14
- __version__ = "0.4.2"
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
- with key.unlock(passphrase):
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)
@@ -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="", entity_type="ai",
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="~/.skcapstone/agent-card.json")
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(),
@@ -20,7 +20,9 @@ def register_peer_commands(main: click.Group) -> None:
20
20
 
21
21
  @main.group()
22
22
  def peer():
23
- """Peer management — discover, add, and manage trusted contacts."""
23
+ """Peer management — discover, add, and manage trusted contacts.
24
+
25
+ Identity-layer peers (PGP keys, trust). For transport routing, see 'peers'."""
24
26
 
25
27
  @peer.command("add")
26
28
  @click.option("--card", "card_path", type=click.Path(exists=True), help="Import from identity card.")
@@ -17,7 +17,9 @@ def register_peers_dir_commands(main: click.Group) -> None:
17
17
 
18
18
  @main.group("peers")
19
19
  def peers_dir():
20
- """Peer transport directory — routing addresses for the mesh."""
20
+ """Peer transport directory — routing addresses for the mesh.
21
+
22
+ SKComm transport endpoints. For identity/trust peers, see 'peer'."""
21
23
 
22
24
  @peers_dir.command("list")
23
25
  @click.option("--home", "sk_home", default=AGENT_HOME, type=click.Path())
@@ -33,8 +33,12 @@ def register_preflight_commands(main: click.Group) -> None:
33
33
  skcapstone preflight --json-out
34
34
  """
35
35
  import json
36
+ from .. import ensure_skeleton
36
37
  from ..preflight import PreflightChecker
37
38
 
39
+ # Ensure all expected directories exist before checking
40
+ ensure_skeleton()
41
+
38
42
  checker = PreflightChecker(home=Path(home).expanduser())
39
43
  summary = checker.run_all()
40
44
 
@@ -50,19 +50,49 @@ def register_skills_commands(main: click.Group) -> None:
50
50
 
51
51
  skcapstone skills list --query identity --json
52
52
  """
53
- client = get_registry_client(registry)
54
- if client is None:
55
- console.print(
56
- "[bold red]skskills not installed.[/] "
57
- "Run: pip install skskills"
58
- )
59
- sys.exit(1)
53
+ # Try remote registry first, fall back to local catalog
54
+ skill_entries = None
55
+ source = "remote"
60
56
 
61
- try:
62
- skill_entries = client.search(query) if query else client.list_skills()
63
- except Exception as exc:
64
- console.print(f"[bold red]Registry error:[/] {exc}")
65
- sys.exit(1)
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)
65
+ if skill_entries is None:
66
+ try:
67
+ from skskills.catalog import SkillCatalog
68
+
69
+ catalog = SkillCatalog()
70
+ if query:
71
+ entries = catalog.search(query)
72
+ else:
73
+ entries = catalog.list_all()
74
+ skill_entries = [
75
+ {
76
+ "name": e.name,
77
+ "version": "",
78
+ "description": e.description,
79
+ "tags": e.tags,
80
+ "category": e.category,
81
+ "pip": e.pip,
82
+ "git": e.git,
83
+ }
84
+ for e in entries
85
+ ]
86
+ source = "catalog"
87
+ except ImportError:
88
+ console.print(
89
+ "[bold red]skskills not installed.[/] "
90
+ "Run: pip install skskills"
91
+ )
92
+ sys.exit(1)
93
+ except Exception as exc:
94
+ console.print(f"[bold red]Catalog error:[/] {exc}")
95
+ sys.exit(1)
66
96
 
67
97
  if json_out:
68
98
  click.echo(json.dumps(skill_entries, indent=2))
@@ -76,17 +106,19 @@ def register_skills_commands(main: click.Group) -> None:
76
106
  label = f"[bold]{len(skill_entries)}[/] skill(s)"
77
107
  if query:
78
108
  label += f" matching [cyan]'{query}'[/]"
109
+ if source == "catalog":
110
+ label += " [dim](local catalog)[/]"
79
111
 
80
112
  table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
81
113
  table.add_column("Name", style="cyan")
82
- table.add_column("Version", style="dim")
114
+ table.add_column("Category", style="dim")
83
115
  table.add_column("Description")
84
116
  table.add_column("Tags", style="dim")
85
117
 
86
118
  for s in skill_entries:
87
119
  table.add_row(
88
120
  s.get("name", ""),
89
- s.get("version", ""),
121
+ s.get("category", s.get("version", "")),
90
122
  s.get("description", ""),
91
123
  ", ".join(s.get("tags", [])),
92
124
  )
@@ -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.")
@@ -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
 
@@ -109,6 +109,52 @@ def _step_identity(home_path: Path, name: str, email: str | None) -> tuple[str,
109
109
 
110
110
  (home_path / "skills").mkdir(parents=True, exist_ok=True)
111
111
 
112
+ # Create full skeleton so all commands work from day one
113
+ agent_slug = name.lower().replace(" ", "-")
114
+ agent_dir = home_path / "agents" / agent_slug
115
+
116
+ skeleton_dirs = [
117
+ # Shared root directories
118
+ home_path / "heartbeats",
119
+ home_path / "peers",
120
+ home_path / "coordination" / "tasks",
121
+ home_path / "coordination" / "agents",
122
+ home_path / "logs",
123
+ home_path / "comms" / "inbox",
124
+ home_path / "comms" / "outbox",
125
+ home_path / "comms" / "archive",
126
+ home_path / "archive",
127
+ home_path / "deployments",
128
+ home_path / "docs",
129
+ home_path / "metrics",
130
+ # Per-agent directories
131
+ agent_dir / "memory" / "short-term",
132
+ agent_dir / "memory" / "mid-term",
133
+ agent_dir / "memory" / "long-term",
134
+ agent_dir / "soul" / "installed",
135
+ agent_dir / "wallet",
136
+ agent_dir / "seeds",
137
+ agent_dir / "identity",
138
+ agent_dir / "config",
139
+ agent_dir / "logs",
140
+ agent_dir / "security",
141
+ agent_dir / "cloud9",
142
+ agent_dir / "trust" / "febs",
143
+ agent_dir / "sync" / "outbox",
144
+ agent_dir / "sync" / "inbox",
145
+ agent_dir / "sync" / "archive",
146
+ agent_dir / "reflections",
147
+ agent_dir / "improvements",
148
+ agent_dir / "scripts",
149
+ agent_dir / "cron",
150
+ agent_dir / "archive",
151
+ agent_dir / "comms" / "inbox",
152
+ agent_dir / "comms" / "outbox",
153
+ agent_dir / "comms" / "archive",
154
+ ]
155
+ for d in skeleton_dirs:
156
+ d.mkdir(parents=True, exist_ok=True)
157
+
112
158
  manifest = {
113
159
  "name": name,
114
160
  "version": __version__,