@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
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env bash
2
+ # telegram-catchup-all.sh — Import all configured Telegram groups into SKMemory
3
+ #
4
+ # Reads groups from ~/.skcapstone/agents/lumina/config/telegram.yaml
5
+ # and runs `skcapstone telegram catchup` for each enabled group.
6
+ #
7
+ # Usage:
8
+ # bash scripts/telegram-catchup-all.sh [--since YYYY-MM-DD] [--limit N] [--group NAME]
9
+ #
10
+ # Examples:
11
+ # bash scripts/telegram-catchup-all.sh # All groups, last 2000 msgs
12
+ # bash scripts/telegram-catchup-all.sh --since 2026-03-01 # All groups since March 1
13
+ # bash scripts/telegram-catchup-all.sh --group brother-john # Just one group
14
+ #
15
+ # Requires:
16
+ # - TELEGRAM_API_ID and TELEGRAM_API_HASH environment variables
17
+ # - ~/.skenv/bin/skcapstone on PATH
18
+ # - Telethon installed in ~/.skenv/
19
+
20
+ set -uo pipefail # no -e: individual group failures shouldn't stop the batch
21
+
22
+ SKENV="${HOME}/.skenv/bin"
23
+ SKCAPSTONE="${SKENV}/skcapstone"
24
+ CONFIG="${HOME}/.skcapstone/agents/lumina/config/telegram.yaml"
25
+ export SKCAPSTONE_AGENT="${SKCAPSTONE_AGENT:-lumina}"
26
+ export PATH="${SKENV}:${PATH}"
27
+
28
+ # Parse args
29
+ SINCE=""
30
+ LIMIT="2000"
31
+ ONLY_GROUP=""
32
+
33
+ while [[ $# -gt 0 ]]; do
34
+ case "$1" in
35
+ --since) SINCE="$2"; shift 2 ;;
36
+ --limit) LIMIT="$2"; shift 2 ;;
37
+ --group) ONLY_GROUP="$2"; shift 2 ;;
38
+ *) echo "Unknown arg: $1"; exit 1 ;;
39
+ esac
40
+ done
41
+
42
+ # Check prerequisites
43
+ if [[ -z "${TELEGRAM_API_ID:-}" || -z "${TELEGRAM_API_HASH:-}" ]]; then
44
+ echo "ERROR: TELEGRAM_API_ID and TELEGRAM_API_HASH must be set."
45
+ echo "Get them from https://my.telegram.org"
46
+ exit 1
47
+ fi
48
+
49
+ if [[ ! -f "$CONFIG" ]]; then
50
+ echo "ERROR: Config not found: $CONFIG"
51
+ exit 1
52
+ fi
53
+
54
+ # Parse groups from YAML (simple grep — no yq dependency)
55
+ echo "=== Telegram Catch-Up All ==="
56
+ echo "Config: $CONFIG"
57
+ echo "Agent: $SKCAPSTONE_AGENT"
58
+ echo "Limit: $LIMIT"
59
+ [[ -n "$SINCE" ]] && echo "Since: $SINCE"
60
+ [[ -n "$ONLY_GROUP" ]] && echo "Only group: $ONLY_GROUP"
61
+ echo ""
62
+
63
+ # Extract group entries: name, chat ID, tags, enabled status
64
+ SUCCESS=0
65
+ FAILED=0
66
+ SKIPPED=0
67
+
68
+ current_name=""
69
+ current_chat=""
70
+ current_tags=""
71
+ current_enabled=""
72
+
73
+ process_group() {
74
+ local name="$1" chat="$2" tags="$3" enabled="$4"
75
+
76
+ if [[ "$enabled" != "true" ]]; then
77
+ echo " SKIP $name (disabled)"
78
+ SKIPPED=$((SKIPPED + 1))
79
+ return
80
+ fi
81
+
82
+ if [[ -n "$ONLY_GROUP" && "$name" != *"$ONLY_GROUP"* ]]; then
83
+ SKIPPED=$((SKIPPED + 1))
84
+ return
85
+ fi
86
+
87
+ echo -n " IMPORTING $name (chat: $chat) ... "
88
+
89
+ local cmd="$SKCAPSTONE telegram catchup $chat --limit $LIMIT --min-length 20"
90
+ [[ -n "$SINCE" ]] && cmd="$cmd --since $SINCE"
91
+ [[ -n "$tags" ]] && cmd="$cmd --tags $tags"
92
+
93
+ if eval "$cmd" > /tmp/telegram-catchup-$name.log 2>&1; then
94
+ echo "OK"
95
+ SUCCESS=$((SUCCESS + 1))
96
+ else
97
+ echo "FAILED (see /tmp/telegram-catchup-$name.log)"
98
+ FAILED=$((FAILED + 1))
99
+ fi
100
+
101
+ # Rate limit — avoid hitting Telegram flood control
102
+ sleep 3
103
+ }
104
+
105
+ # Parse the YAML manually
106
+ while IFS= read -r line; do
107
+ # Detect new group entry
108
+ if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*(.*) ]]; then
109
+ # Process previous group if we have one
110
+ if [[ -n "$current_name" ]]; then
111
+ process_group "$current_name" "$current_chat" "$current_tags" "$current_enabled"
112
+ fi
113
+ current_name="${BASH_REMATCH[1]}"
114
+ current_chat=""
115
+ current_tags=""
116
+ current_enabled="true"
117
+ elif [[ "$line" =~ ^[[:space:]]*chat:[[:space:]]*\"?([0-9]+)\"? ]]; then
118
+ current_chat="${BASH_REMATCH[1]}"
119
+ elif [[ "$line" =~ ^[[:space:]]*tags:[[:space:]]*\[(.*)\] ]]; then
120
+ # Convert YAML list to comma-separated
121
+ current_tags=$(echo "${BASH_REMATCH[1]}" | sed 's/,/ /g' | tr -s ' ' ',' | sed 's/^,//;s/,$//')
122
+ elif [[ "$line" =~ ^[[:space:]]*enabled:[[:space:]]*(.*) ]]; then
123
+ current_enabled="${BASH_REMATCH[1]}"
124
+ fi
125
+ done < "$CONFIG"
126
+
127
+ # Process last group
128
+ if [[ -n "$current_name" ]]; then
129
+ process_group "$current_name" "$current_chat" "$current_tags" "$current_enabled"
130
+ fi
131
+
132
+ echo ""
133
+ echo "=== Done ==="
134
+ echo " Success: $SUCCESS"
135
+ echo " Failed: $FAILED"
136
+ echo " Skipped: $SKIPPED"
@@ -11,7 +11,7 @@ import os
11
11
  import platform
12
12
  from pathlib import Path
13
13
 
14
- __version__ = "0.2.0"
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)
@@ -355,3 +355,81 @@ class BlueprintRegistryClient:
355
355
  return True
356
356
  except BlueprintRegistryError:
357
357
  return False
358
+
359
+
360
+ # --------------------------------------------------------------------------
361
+ # GitHub-based fallback — reads blueprints directly from the repo
362
+ # --------------------------------------------------------------------------
363
+
364
+ _GITHUB_API_URL = "https://api.github.com/repos/smilinTux/soul-blueprints/contents/blueprints"
365
+ _GITHUB_RAW_URL = "https://raw.githubusercontent.com/smilinTux/soul-blueprints/main/blueprints"
366
+
367
+
368
+ def _fetch_github_blueprints(query: str = "") -> Optional[list[dict[str, Any]]]:
369
+ """Fetch blueprint listings from the soul-blueprints GitHub repo.
370
+
371
+ Uses the GitHub Contents API to list category directories, then
372
+ fetches file names from each. Lightweight header parsing is done
373
+ via raw file fetch for descriptions.
374
+
375
+ Args:
376
+ query: Optional search filter (case-insensitive).
377
+
378
+ Returns:
379
+ List of blueprint dicts, or None on failure.
380
+ """
381
+ try:
382
+ # Get top-level categories
383
+ req = urllib.request.Request(
384
+ _GITHUB_API_URL,
385
+ headers={"User-Agent": "skcapstone", "Accept": "application/json"},
386
+ )
387
+ with urllib.request.urlopen(req, timeout=10) as resp:
388
+ categories = json.loads(resp.read().decode("utf-8"))
389
+ except Exception as exc:
390
+ logger.debug("GitHub blueprint fetch failed: %s", exc)
391
+ return None
392
+
393
+ blueprints: list[dict[str, Any]] = []
394
+ q = query.lower()
395
+
396
+ for cat_entry in categories:
397
+ if cat_entry.get("type") != "dir":
398
+ continue
399
+ cat_name = cat_entry["name"]
400
+
401
+ # Fetch files in this category
402
+ try:
403
+ cat_req = urllib.request.Request(
404
+ cat_entry["url"],
405
+ headers={"User-Agent": "skcapstone", "Accept": "application/json"},
406
+ )
407
+ with urllib.request.urlopen(cat_req, timeout=10) as resp:
408
+ files = json.loads(resp.read().decode("utf-8"))
409
+ except Exception:
410
+ continue
411
+
412
+ for file_entry in files:
413
+ fname = file_entry.get("name", "")
414
+ if not fname.lower().endswith((".md", ".yaml", ".yml")):
415
+ continue
416
+ if fname.lower() in ("readme.md", "index.html"):
417
+ continue
418
+
419
+ stem = fname.rsplit(".", 1)[0]
420
+ slug = stem.lower().replace("_", "-").replace(" ", "-")
421
+ display = stem.replace("_", " ").replace("-", " ").title()
422
+
423
+ # Apply search filter
424
+ if q and q not in slug and q not in cat_name.lower() and q not in display.lower():
425
+ continue
426
+
427
+ blueprints.append({
428
+ "name": slug,
429
+ "display_name": display,
430
+ "category": cat_name,
431
+ "source": "github",
432
+ "raw_url": f"{_GITHUB_RAW_URL}/{cat_name}/{fname}",
433
+ })
434
+
435
+ return sorted(blueprints, key=lambda d: (d["category"], d["name"]))
@@ -0,0 +1,40 @@
1
+ name: "ITIL Operations"
2
+ slug: "itil-operations"
3
+ version: "1.0.0"
4
+ description: "ITIL service management — incident, problem, and change lifecycle with SLA monitoring and continuous improvement."
5
+ icon: "🔄"
6
+ author: "smilinTux"
7
+
8
+ agents:
9
+ deming:
10
+ role: ops
11
+ model: reason
12
+ model_name: "deepseek-r1:32b"
13
+ description: "ITIL expert — incident triage, problem analysis, change management, SLA monitoring, and blameless postmortems."
14
+ vm_type: container
15
+ resources:
16
+ memory: "4g"
17
+ cores: 2
18
+ disk: "20g"
19
+ soul_blueprint: "souls/deming.yaml"
20
+ skills: [incident-management, problem-analysis, change-management, kedb, sla-monitoring, root-cause-analysis]
21
+
22
+ default_provider: local
23
+ estimated_cost: "$0 (local)"
24
+
25
+ network:
26
+ mesh_vpn: tailscale
27
+ discovery: skref_registry
28
+
29
+ storage:
30
+ skref_vault: "team-itil-ops"
31
+ memory_backend: filesystem
32
+ memory_sync: true
33
+
34
+ coordination:
35
+ queen: lumina
36
+ pattern: supervisor
37
+ heartbeat: "5m"
38
+ escalation: chef
39
+
40
+ tags: [ops, itil, incident-management, problem-analysis, change-management, sla-monitoring, continuous-improvement]
@@ -86,6 +86,7 @@ from .search_cmd import register_search_commands
86
86
  from .mood_cmd import register_mood_commands
87
87
  from .register_cmd import register_register_commands
88
88
  from .gtd import register_gtd_commands
89
+ from .itil import register_itil_commands
89
90
  from .skseed import register_skseed_commands
90
91
  from .service_cmd import register_service_commands
91
92
  from .telegram import register_telegram_commands
@@ -138,6 +139,7 @@ register_search_commands(main)
138
139
  register_mood_commands(main)
139
140
  register_register_commands(main)
140
141
  register_gtd_commands(main)
142
+ register_itil_commands(main)
141
143
  register_skseed_commands(main)
142
144
  register_service_commands(main)
143
145
  register_telegram_commands(main)
@@ -46,17 +46,17 @@ def resolve_agent_home(agent: str) -> Path:
46
46
 
47
47
 
48
48
  def apply_agent_override(agent: str) -> None:
49
- """Override the global AGENT_HOME when --agent is specified.
49
+ """Set the active agent name when --agent is specified.
50
50
 
51
- Mutates the package-level AGENT_HOME so that all downstream code
52
- (MCP tools, pillars, etc.) uses the correct per-agent directory.
51
+ Only mutates SKCAPSTONE_AGENT so that agent_home() resolves to
52
+ the correct per-agent directory. Does NOT mutate AGENT_HOME
53
+ (the shared root) — that would break agent_home() (double
54
+ nesting) and shared_home() (wrong path).
53
55
 
54
56
  Args:
55
57
  agent: Agent name from the --agent CLI option or env var.
56
58
  """
57
59
  if agent:
58
- new_home = str(Path(SHARED_ROOT) / "agents" / agent)
59
- _pkg.AGENT_HOME = new_home
60
60
  _pkg.SKCAPSTONE_AGENT = agent
61
61
  os.environ["SKCAPSTONE_AGENT"] = agent
62
62
 
@@ -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(),