@smilintux/skcapstone 0.9.0 → 0.12.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/.env.example +10 -4
- package/.github/workflows/ci.yml +2 -2
- package/.github/workflows/publish.yml +9 -2
- package/.openclaw-workspace.json +2 -2
- package/CLAUDE.md +37 -0
- package/MISSION.md +17 -2
- package/README.md +282 -3
- package/docker/Dockerfile +7 -7
- package/docker/compose-templates/dev-team.yml +12 -12
- package/docker/compose-templates/mini-team.yml +9 -9
- package/docker/compose-templates/ops-team.yml +10 -10
- package/docker/compose-templates/research-team.yml +10 -10
- package/docker/entrypoint.sh +4 -4
- package/docs/ADR-optional-integration-backbone.md +181 -0
- package/docs/ARCHITECTURE.md +186 -43
- package/docs/BOND_WITH_GROK.md +6 -6
- package/docs/CUSTOM_AGENT.md +278 -1
- package/docs/DREAMING.md +70 -0
- package/docs/GETTING_STARTED.md +10 -7
- package/docs/QUICKSTART.md +10 -6
- package/docs/SKJOULE_ARCHITECTURE.md +3 -3
- package/docs/SOUL_SWAPPER.md +5 -5
- package/docs/hammertime-audit.md +402 -0
- package/docs/sk-integration-HANDOFF.md +117 -0
- package/docs/skscheduler.md +155 -0
- package/docs/superpowers/examples/jobs.yaml +31 -0
- package/docs/superpowers/plans/2026-06-08-skscheduler.md +1265 -0
- package/docs/superpowers/specs/2026-06-08-skscheduler-design.md +186 -0
- package/examples/custom-bond-template.json +1 -1
- package/examples/grok-feb.json +1 -1
- package/examples/queen-ava-feb.json +1 -1
- package/launchd/com.skcapstone.daemon.plist +52 -0
- package/launchd/com.skcapstone.memory-compress.plist +45 -0
- package/launchd/com.skcapstone.skcomms-heartbeat.plist +33 -0
- package/launchd/com.skcapstone.skcomms-queue-drain.plist +34 -0
- package/launchd/install-launchd.sh +156 -0
- package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/index.ts +3 -2
- package/package.json +1 -1
- package/pyproject.toml +16 -10
- package/scripts/archive-sessions.sh +95 -0
- package/scripts/check-updates.py +4 -4
- package/scripts/install-bundle.sh +8 -8
- package/scripts/install.ps1 +12 -11
- package/scripts/install.sh +196 -11
- package/scripts/model-fallback-monitor.sh +102 -0
- package/scripts/notion-api.py +259 -0
- package/scripts/nvidia-proxy.mjs +908 -0
- package/scripts/proxy-monitor.sh +89 -0
- package/scripts/refresh-anthropic-token.sh +172 -0
- package/scripts/release.sh +98 -0
- package/scripts/session-to-memory.py +219 -0
- package/scripts/skgateway.mjs +856 -0
- package/scripts/telegram-catchup-all.sh +147 -0
- package/scripts/verify_install.sh +2 -2
- package/scripts/wargov-ufo-capture/README.md +43 -0
- package/scripts/wargov-ufo-capture/cdp_capture_release2.py +273 -0
- package/scripts/wargov-ufo-capture/cdp_capture_splc_doj.py +246 -0
- package/scripts/wargov-ufo-capture/cdp_finish.py +271 -0
- package/scripts/wargov-ufo-capture/cdp_probe.py +188 -0
- package/scripts/wargov-ufo-capture/cdp_splc_pressrelease.py +101 -0
- package/scripts/wargov-ufo-capture/parse_csv.py +95 -0
- package/scripts/wargov-ufo-capture/pull_dvids.sh +107 -0
- package/scripts/watch-anthropic-token.sh +212 -0
- package/scripts/windows/install-tasks.ps1 +7 -7
- package/scripts/windows/skcapstone-task.xml +1 -1
- package/src/skcapstone/__init__.py +45 -3
- package/src/skcapstone/_cli_monolith.py +20 -15
- package/src/skcapstone/activity.py +5 -1
- package/src/skcapstone/agent_card.py +3 -2
- package/src/skcapstone/api.py +41 -40
- package/src/skcapstone/auction.py +14 -11
- package/src/skcapstone/backup.py +2 -1
- package/src/skcapstone/blueprint_registry.py +4 -3
- package/src/skcapstone/blueprints/builtins/itil-operations.yaml +40 -0
- package/src/skcapstone/brain_first.py +238 -0
- package/src/skcapstone/changelog.py +1 -1
- package/src/skcapstone/chat.py +22 -17
- package/src/skcapstone/cli/__init__.py +9 -1
- package/src/skcapstone/cli/_common.py +1 -0
- package/src/skcapstone/cli/agents_spawner.py +5 -2
- package/src/skcapstone/cli/alerts.py +25 -4
- package/src/skcapstone/cli/bench.py +15 -15
- package/src/skcapstone/cli/chat.py +7 -4
- package/src/skcapstone/cli/consciousness.py +5 -2
- package/src/skcapstone/cli/context_cmd.py +18 -4
- package/src/skcapstone/cli/daemon.py +121 -42
- package/src/skcapstone/cli/gtd.py +26 -1
- package/src/skcapstone/cli/housekeeping.py +3 -3
- package/src/skcapstone/cli/identity_cmd.py +378 -0
- package/src/skcapstone/cli/joule_cmd.py +7 -3
- package/src/skcapstone/cli/memory.py +8 -6
- package/src/skcapstone/cli/peers_dir.py +1 -1
- package/src/skcapstone/cli/register_cmd.py +29 -3
- package/src/skcapstone/cli/scheduler_cmd.py +167 -0
- package/src/skcapstone/cli/session.py +25 -0
- package/src/skcapstone/cli/setup.py +96 -29
- package/src/skcapstone/cli/shell_cmd.py +53 -1
- package/src/skcapstone/cli/skills_cmd.py +2 -2
- package/src/skcapstone/cli/soul.py +8 -5
- package/src/skcapstone/cli/status.py +37 -11
- package/src/skcapstone/cli/telegram.py +21 -0
- package/src/skcapstone/cli/test_cmd.py +5 -5
- package/src/skcapstone/cli/test_connection.py +2 -2
- package/src/skcapstone/cli/upgrade_cmd.py +23 -14
- package/src/skcapstone/cli/version_cmd.py +1 -1
- package/src/skcapstone/cli/watch_cmd.py +9 -6
- package/src/skcapstone/cloud9_bridge.py +14 -14
- package/src/skcapstone/codex_setup.py +255 -0
- package/src/skcapstone/config_validator.py +7 -4
- package/src/skcapstone/consciousness_config.py +5 -1
- package/src/skcapstone/consciousness_loop.py +313 -273
- package/src/skcapstone/context_loader.py +121 -0
- package/src/skcapstone/coord_federation.py +2 -1
- package/src/skcapstone/coordination.py +23 -6
- package/src/skcapstone/crush_integration.py +2 -1
- package/src/skcapstone/daemon.py +151 -88
- package/src/skcapstone/dashboard.py +10 -10
- package/src/skcapstone/data/sk-agent-picker.sh +421 -0
- package/src/skcapstone/data/systemd/skcapstone-api.socket +9 -0
- package/src/skcapstone/data/systemd/skcapstone-memory-compress.service +18 -0
- package/src/skcapstone/data/systemd/skcapstone-memory-compress.timer +11 -0
- package/src/skcapstone/data/systemd/skcapstone.service +37 -0
- package/src/skcapstone/data/systemd/skcapstone@.service +50 -0
- package/src/skcapstone/data/systemd/skcomms-heartbeat.service +18 -0
- package/{systemd/skcomm-heartbeat.timer → src/skcapstone/data/systemd/skcomms-heartbeat.timer} +2 -2
- package/src/skcapstone/data/systemd/skcomms-queue-drain.service +17 -0
- package/{systemd/skcomm-queue-drain.timer → src/skcapstone/data/systemd/skcomms-queue-drain.timer} +2 -2
- package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
- package/src/skcapstone/defaults/claude/settings.json +74 -0
- package/src/skcapstone/defaults/lumina/config/claude-hooks.md +57 -0
- package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
- package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +2 -2
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +2 -2
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +9 -9
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +2 -2
- package/src/skcapstone/defaults/unhinged.json +13 -0
- package/src/skcapstone/discovery.py +43 -20
- package/src/skcapstone/doctor.py +941 -22
- package/src/skcapstone/dreaming.py +1183 -109
- package/src/skcapstone/emotion_tracker.py +2 -2
- package/src/skcapstone/export.py +4 -3
- package/src/skcapstone/fuse_mount.py +35 -25
- package/src/skcapstone/gui_installer.py +2 -2
- package/src/skcapstone/heartbeat.py +34 -30
- package/src/skcapstone/housekeeping.py +14 -14
- package/src/skcapstone/install_wizard.py +209 -7
- package/src/skcapstone/itil.py +13 -4
- package/src/skcapstone/kms_scheduler.py +10 -8
- package/src/skcapstone/launchd.py +426 -0
- package/src/skcapstone/mcp_launcher.py +15 -1
- package/src/skcapstone/mcp_server.py +341 -49
- package/src/skcapstone/mcp_tools/__init__.py +2 -0
- package/src/skcapstone/mcp_tools/_helpers.py +2 -2
- package/src/skcapstone/mcp_tools/ansible_tools.py +7 -4
- package/src/skcapstone/mcp_tools/brain_first_tools.py +90 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +7 -4
- package/src/skcapstone/mcp_tools/comm_tools.py +10 -10
- package/src/skcapstone/mcp_tools/coord_tools.py +8 -4
- package/src/skcapstone/mcp_tools/did_tools.py +11 -8
- package/src/skcapstone/mcp_tools/gtd_tools.py +4 -4
- package/src/skcapstone/mcp_tools/memory_tools.py +6 -2
- package/src/skcapstone/mcp_tools/notification_tools.py +22 -6
- package/src/skcapstone/mcp_tools/{skcomm_tools.py → skcomms_tools.py} +14 -14
- package/src/skcapstone/mcp_tools/soul_tools.py +8 -2
- package/src/skcapstone/mdns_discovery.py +2 -2
- package/src/skcapstone/memory_curator.py +1 -1
- package/src/skcapstone/memory_engine.py +10 -3
- package/src/skcapstone/metrics.py +30 -16
- package/src/skcapstone/migrate_memories.py +4 -3
- package/src/skcapstone/migrate_multi_agent.py +8 -7
- package/src/skcapstone/models.py +47 -5
- package/src/skcapstone/notifications.py +42 -18
- package/src/skcapstone/onboard.py +1000 -126
- package/src/skcapstone/operator_link.py +170 -0
- package/src/skcapstone/peer_directory.py +4 -4
- package/src/skcapstone/peers.py +19 -19
- package/src/skcapstone/pillars/__init__.py +7 -5
- package/src/skcapstone/pillars/consciousness.py +191 -0
- package/src/skcapstone/pillars/identity.py +51 -7
- package/src/skcapstone/pillars/memory.py +9 -3
- package/src/skcapstone/pillars/sync.py +2 -2
- package/src/skcapstone/preflight.py +3 -3
- package/src/skcapstone/providers/docker.py +28 -28
- package/src/skcapstone/register.py +6 -6
- package/src/skcapstone/registry_client.py +5 -4
- package/src/skcapstone/runtime.py +14 -3
- package/src/skcapstone/scheduled_tasks.py +254 -19
- package/src/skcapstone/scheduler_jobs.py +456 -0
- package/src/skcapstone/scheduler_runner.py +239 -0
- package/src/skcapstone/scheduler_state.py +162 -0
- package/src/skcapstone/sdk.py +310 -0
- package/src/skcapstone/service_health.py +279 -39
- package/src/skcapstone/session_briefing.py +108 -0
- package/src/skcapstone/session_capture.py +1 -1
- package/src/skcapstone/shell.py +7 -1
- package/src/skcapstone/soul.py +3 -1
- package/src/skcapstone/soul_switch.py +3 -1
- package/src/skcapstone/summary.py +6 -6
- package/src/skcapstone/sync_engine.py +15 -15
- package/src/skcapstone/sync_watcher.py +2 -2
- package/src/skcapstone/systemd.py +72 -21
- package/src/skcapstone/team_comms.py +8 -8
- package/src/skcapstone/team_engine.py +1 -1
- package/src/skcapstone/testrunner.py +3 -3
- package/src/skcapstone/trust_graph.py +40 -5
- package/src/skcapstone/unified_search.py +15 -6
- package/src/skcapstone/uninstall_wizard.py +11 -3
- package/src/skcapstone/version_check.py +8 -4
- package/src/skcapstone/warmth_anchor.py +4 -2
- package/src/skcapstone/whoami.py +4 -4
- package/systemd/skcapstone.service +4 -6
- package/systemd/skcapstone@.service +7 -8
- package/systemd/skcomms-heartbeat.service +21 -0
- package/systemd/skcomms-heartbeat.timer +12 -0
- package/systemd/skcomms-queue-drain.service +17 -0
- package/systemd/skcomms-queue-drain.timer +12 -0
- package/tests/conftest.py +39 -0
- package/tests/integration/test_consciousness_e2e.py +39 -39
- package/tests/test_agent_card.py +1 -1
- package/tests/test_agent_home_scaffold.py +34 -0
- package/tests/test_alerts_consumer_topics.py +27 -0
- package/tests/test_backup.py +2 -1
- package/tests/test_chat.py +6 -6
- package/tests/test_claude_md.py +2 -2
- package/tests/test_cli_skills.py +10 -10
- package/tests/test_cli_test_cmd.py +4 -4
- package/tests/test_cli_test_connection.py +1 -1
- package/tests/test_cloud9_bridge.py +6 -6
- package/tests/test_consciousness_e2e.py +1 -1
- package/tests/test_consciousness_loop.py +10 -10
- package/tests/test_coordination.py +25 -0
- package/tests/test_cross_package.py +21 -21
- package/tests/test_daemon.py +4 -4
- package/tests/test_daemon_shutdown.py +1 -1
- package/tests/test_docker_provider.py +29 -29
- package/tests/test_doctor.py +400 -0
- package/tests/test_doctor_skscheduler.py +50 -0
- package/tests/test_dreaming_engine.py +147 -0
- package/tests/test_dreaming_gtd_capture.py +35 -0
- package/tests/test_e2e_automated.py +8 -5
- package/tests/test_fuse_mount.py +10 -10
- package/tests/test_gtd_brief.py +46 -0
- package/tests/test_gtd_malformed_tolerance.py +31 -0
- package/tests/test_housekeeping.py +15 -15
- package/tests/test_identity_migrate.py +251 -0
- package/tests/test_integration_backbone.py +598 -0
- package/tests/test_itil_gtd_lifecycle.py +37 -0
- package/tests/test_jobs_dropins.py +84 -0
- package/tests/test_mcp_server.py +82 -37
- package/tests/test_models.py +48 -4
- package/tests/test_multi_agent.py +31 -29
- package/tests/test_notifications.py +122 -32
- package/tests/test_onboard.py +63 -75
- package/tests/test_operator_link.py +78 -0
- package/tests/test_peers.py +14 -14
- package/tests/test_pillars.py +98 -0
- package/tests/test_preflight.py +3 -3
- package/tests/test_runtime.py +21 -0
- package/tests/test_scheduled_tasks.py +11 -6
- package/tests/test_scheduler_cli.py +47 -0
- package/tests/test_scheduler_features.py +133 -0
- package/tests/test_scheduler_integration.py +87 -0
- package/tests/test_scheduler_jobs.py +155 -0
- package/tests/test_scheduler_runner.py +64 -0
- package/tests/test_scheduler_state.py +57 -0
- package/tests/test_sdk.py +70 -0
- package/tests/test_service_health_incidents.py +34 -0
- package/tests/test_service_registry.py +52 -0
- package/tests/test_session_briefing.py +130 -0
- package/tests/test_snapshots.py +4 -4
- package/tests/test_sync_pipeline.py +26 -26
- package/tests/test_team_comms.py +2 -2
- package/tests/test_testrunner.py +2 -2
- package/tests/test_trust_graph.py +18 -0
- package/tests/test_unified_search.py +2 -2
- package/tests/test_version_check.py +10 -0
- package/tests/test_version_cmd.py +8 -8
- package/tests/test_whoami.py +1 -1
- package/systemd/skcomm-heartbeat.service +0 -18
- package/systemd/skcomm-queue-drain.service +0 -17
- /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/package.json +0 -0
- /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/openclaw.plugin.json +0 -0
|
@@ -140,7 +140,8 @@ def register_daemon_commands(main: click.Group) -> None:
|
|
|
140
140
|
effective_port = _resolve_agent_port(agent, port)
|
|
141
141
|
|
|
142
142
|
if agent:
|
|
143
|
-
# Propagate identity to child imports that read
|
|
143
|
+
# Propagate identity to child imports that read SKAGENT.
|
|
144
|
+
os.environ["SKAGENT"] = agent
|
|
144
145
|
os.environ["SKCAPSTONE_AGENT"] = agent
|
|
145
146
|
|
|
146
147
|
if not home_path.exists():
|
|
@@ -270,60 +271,120 @@ def register_daemon_commands(main: click.Group) -> None:
|
|
|
270
271
|
console.print(f" [yellow]API unreachable on port {effective_port}[/]\n")
|
|
271
272
|
|
|
272
273
|
@daemon.command("install")
|
|
273
|
-
|
|
274
|
-
|
|
274
|
+
@click.option("--agent", "agent_name", default=None,
|
|
275
|
+
help="Agent name for SKCAPSTONE_AGENT (default: from env or 'sovereign').")
|
|
276
|
+
@click.option("--start", is_flag=True, help="Start services immediately after installing.")
|
|
277
|
+
def daemon_install(agent_name: str | None, start: bool):
|
|
278
|
+
"""Install the daemon as a system service.
|
|
275
279
|
|
|
276
|
-
|
|
277
|
-
|
|
280
|
+
On Linux: installs systemd user service units.
|
|
281
|
+
On macOS: installs launchd plist files to ~/Library/LaunchAgents/.
|
|
282
|
+
|
|
283
|
+
The --agent flag sets the SKCAPSTONE_AGENT environment variable
|
|
284
|
+
in the service definition. If not provided, uses the
|
|
285
|
+
SKCAPSTONE_AGENT env var or defaults to 'sovereign'.
|
|
278
286
|
|
|
279
287
|
Examples:
|
|
280
288
|
|
|
281
289
|
skcapstone daemon install
|
|
290
|
+
|
|
291
|
+
skcapstone daemon install --agent myagent --start
|
|
282
292
|
"""
|
|
283
|
-
|
|
293
|
+
import platform
|
|
284
294
|
|
|
285
|
-
|
|
286
|
-
console.print("[red]systemd user session not available.[/]")
|
|
287
|
-
console.print("[dim]This command requires a Linux system with systemd.[/]")
|
|
288
|
-
raise SystemExit(1)
|
|
295
|
+
effective_agent = agent_name or os.environ.get("SKCAPSTONE_AGENT", "sovereign")
|
|
289
296
|
|
|
290
|
-
|
|
291
|
-
|
|
297
|
+
if platform.system() == "Darwin":
|
|
298
|
+
from ..launchd import install_service as launchd_install
|
|
292
299
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
+
console.print(f"\n[cyan]Installing launchd services for agent '{effective_agent}'...[/]")
|
|
301
|
+
result = launchd_install(agent_name=effective_agent, start=start)
|
|
302
|
+
|
|
303
|
+
if result["installed"]:
|
|
304
|
+
for svc in result.get("services", []):
|
|
305
|
+
status = "[green]loaded[/]" if svc.get("loaded") else "[green]installed[/]"
|
|
306
|
+
console.print(f" [green]✓[/] {svc['label']} — {status}")
|
|
307
|
+
console.print()
|
|
308
|
+
console.print("[dim] Manage: launchctl list | grep skcapstone[/]")
|
|
309
|
+
if not start:
|
|
310
|
+
console.print("[dim] Start: launchctl start com.skcapstone.daemon[/]")
|
|
311
|
+
console.print("[dim] Or re-run with --start to load immediately.[/]")
|
|
312
|
+
else:
|
|
313
|
+
console.print("[red]Installation failed. Check logs.[/]")
|
|
314
|
+
raise SystemExit(1)
|
|
315
|
+
console.print()
|
|
316
|
+
|
|
317
|
+
elif platform.system() == "Linux":
|
|
318
|
+
from ..systemd import install_service, systemd_available, SERVICE_NAME
|
|
319
|
+
|
|
320
|
+
if not systemd_available():
|
|
321
|
+
console.print("[red]systemd user session not available.[/]")
|
|
322
|
+
console.print("[dim]This command requires a Linux system with systemd.[/]")
|
|
323
|
+
raise SystemExit(1)
|
|
324
|
+
|
|
325
|
+
console.print(f"\n[cyan]Installing skcapstone systemd service for agent '{effective_agent}'...[/]")
|
|
326
|
+
result = install_service(agent_name=effective_agent, start=start)
|
|
327
|
+
svc_name = result.get("service_name", SERVICE_NAME)
|
|
328
|
+
|
|
329
|
+
if result["installed"]:
|
|
330
|
+
console.print(f"[green] Unit files installed ({svc_name}).[/]")
|
|
331
|
+
if result["enabled"]:
|
|
332
|
+
console.print(f"[green] Service enabled at login.[/]")
|
|
333
|
+
if result.get("started"):
|
|
334
|
+
console.print(f"[green] Service started.[/]")
|
|
335
|
+
else:
|
|
336
|
+
console.print(f"[dim] Start: systemctl --user start {svc_name}[/]")
|
|
337
|
+
console.print()
|
|
300
338
|
|
|
301
|
-
|
|
302
|
-
|
|
339
|
+
if not result["installed"]:
|
|
340
|
+
console.print("[red]Installation failed. Check logs.[/]")
|
|
341
|
+
raise SystemExit(1)
|
|
342
|
+
else:
|
|
343
|
+
console.print(f"[red]Auto-start not supported on {platform.system()}.[/]")
|
|
303
344
|
raise SystemExit(1)
|
|
304
345
|
|
|
305
346
|
@daemon.command("uninstall")
|
|
306
347
|
def daemon_uninstall():
|
|
307
|
-
"""Uninstall the
|
|
348
|
+
"""Uninstall the system service.
|
|
308
349
|
|
|
309
|
-
|
|
350
|
+
On Linux: stops, disables, and removes systemd unit files.
|
|
351
|
+
On macOS: unloads and removes launchd plist files.
|
|
310
352
|
|
|
311
353
|
Examples:
|
|
312
354
|
|
|
313
355
|
skcapstone daemon uninstall
|
|
314
356
|
"""
|
|
315
|
-
|
|
357
|
+
import platform
|
|
316
358
|
|
|
317
|
-
|
|
318
|
-
|
|
359
|
+
if platform.system() == "Darwin":
|
|
360
|
+
from ..launchd import uninstall_service as launchd_uninstall
|
|
319
361
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
362
|
+
console.print("\n[cyan]Uninstalling skcapstone launchd services...[/]")
|
|
363
|
+
result = launchd_uninstall()
|
|
364
|
+
|
|
365
|
+
if result["stopped"]:
|
|
366
|
+
console.print("[green] Services unloaded.[/]")
|
|
367
|
+
if result["removed"]:
|
|
368
|
+
for label in result.get("services", []):
|
|
369
|
+
console.print(f" [green]✓[/] Removed {label}")
|
|
370
|
+
console.print()
|
|
371
|
+
|
|
372
|
+
elif platform.system() == "Linux":
|
|
373
|
+
from ..systemd import uninstall_service
|
|
374
|
+
|
|
375
|
+
console.print("\n[cyan]Uninstalling skcapstone systemd service...[/]")
|
|
376
|
+
result = uninstall_service()
|
|
377
|
+
|
|
378
|
+
if result["stopped"]:
|
|
379
|
+
console.print("[green] Service stopped.[/]")
|
|
380
|
+
if result["disabled"]:
|
|
381
|
+
console.print("[green] Service disabled.[/]")
|
|
382
|
+
if result["removed"]:
|
|
383
|
+
console.print("[green] Unit files removed.[/]")
|
|
384
|
+
console.print()
|
|
385
|
+
|
|
386
|
+
else:
|
|
387
|
+
console.print(f"[red]Not supported on {platform.system()}.[/]")
|
|
327
388
|
|
|
328
389
|
@daemon.command("components")
|
|
329
390
|
@click.option("--agent", default=None, help="Named agent to query.")
|
|
@@ -413,7 +474,10 @@ def register_daemon_commands(main: click.Group) -> None:
|
|
|
413
474
|
@click.option("--lines", "-n", default=50, help="Number of lines (default: 50).")
|
|
414
475
|
@click.option("--follow", "-f", is_flag=True, help="Show the command to follow logs live.")
|
|
415
476
|
def daemon_logs(lines: int, follow: bool):
|
|
416
|
-
"""Show daemon logs
|
|
477
|
+
"""Show daemon logs.
|
|
478
|
+
|
|
479
|
+
On Linux: reads from journald.
|
|
480
|
+
On macOS: reads from ~/.skcapstone/logs/ files.
|
|
417
481
|
|
|
418
482
|
Examples:
|
|
419
483
|
|
|
@@ -423,14 +487,29 @@ def register_daemon_commands(main: click.Group) -> None:
|
|
|
423
487
|
|
|
424
488
|
skcapstone daemon logs -f
|
|
425
489
|
"""
|
|
426
|
-
|
|
490
|
+
import platform
|
|
427
491
|
|
|
428
|
-
if
|
|
429
|
-
|
|
430
|
-
|
|
492
|
+
if platform.system() == "Darwin":
|
|
493
|
+
if follow:
|
|
494
|
+
log_path = Path.home() / ".skcapstone" / "logs" / "daemon.stdout.log"
|
|
495
|
+
console.print(f"\n Run: [bold cyan]tail -f {log_path}[/]\n")
|
|
496
|
+
else:
|
|
497
|
+
from ..launchd import service_logs
|
|
498
|
+
output = service_logs(lines=lines)
|
|
499
|
+
if output.strip():
|
|
500
|
+
click.echo(output)
|
|
501
|
+
else:
|
|
502
|
+
console.print("[dim]No logs found in ~/.skcapstone/logs/[/]")
|
|
503
|
+
console.print("[dim]Is the service installed? Run: skcapstone daemon install[/]")
|
|
431
504
|
else:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
505
|
+
from ..systemd import service_logs
|
|
506
|
+
|
|
507
|
+
if follow:
|
|
508
|
+
cmd = service_logs(follow=True)
|
|
509
|
+
console.print(f"\n Run: [bold cyan]{cmd}[/]\n")
|
|
435
510
|
else:
|
|
436
|
-
|
|
511
|
+
output = service_logs(lines=lines)
|
|
512
|
+
if output.strip():
|
|
513
|
+
click.echo(output)
|
|
514
|
+
else:
|
|
515
|
+
console.print("[dim]No logs found. Is the service installed?[/]")
|
|
@@ -108,10 +108,35 @@ def register_gtd_commands(main: click.Group) -> None:
|
|
|
108
108
|
console.print()
|
|
109
109
|
|
|
110
110
|
@gtd.command("status")
|
|
111
|
-
|
|
111
|
+
@click.option("--brief", is_flag=True,
|
|
112
|
+
help="One-line summary (for hooks / session start).")
|
|
113
|
+
def gtd_status(brief: bool):
|
|
112
114
|
"""Summary of all GTD lists."""
|
|
113
115
|
from ..mcp_tools.gtd_tools import _load_list, _GTD_LISTS
|
|
114
116
|
|
|
117
|
+
if brief:
|
|
118
|
+
from datetime import datetime, timezone
|
|
119
|
+
|
|
120
|
+
counts = {name: len(_load_list(name)) for name in _GTD_LISTS}
|
|
121
|
+
now = datetime.now(timezone.utc)
|
|
122
|
+
stale = 0
|
|
123
|
+
for p in _load_list("projects"):
|
|
124
|
+
ts = p.get("moved_at") or p.get("created_at")
|
|
125
|
+
try:
|
|
126
|
+
if ts and (now - datetime.fromisoformat(ts)).days >= 7:
|
|
127
|
+
stale += 1
|
|
128
|
+
except (ValueError, TypeError):
|
|
129
|
+
pass
|
|
130
|
+
stale_str = f" ({stale} stale)" if stale else ""
|
|
131
|
+
click.echo(
|
|
132
|
+
f"GTD: {counts.get('inbox', 0)} inbox · "
|
|
133
|
+
f"{counts.get('next-actions', 0)} next · "
|
|
134
|
+
f"{counts.get('projects', 0)} projects{stale_str} · "
|
|
135
|
+
f"{counts.get('waiting-for', 0)} waiting · "
|
|
136
|
+
f"{counts.get('someday-maybe', 0)} someday"
|
|
137
|
+
)
|
|
138
|
+
return
|
|
139
|
+
|
|
115
140
|
console.print()
|
|
116
141
|
total = 0
|
|
117
142
|
rows = []
|
|
@@ -14,9 +14,9 @@ def register_housekeeping_commands(main: click.Group) -> None:
|
|
|
14
14
|
|
|
15
15
|
@main.command("housekeeping")
|
|
16
16
|
@click.option("--home", default=AGENT_HOME, type=click.Path(), help="Agent home directory.")
|
|
17
|
-
@click.option("--
|
|
17
|
+
@click.option("--skcomms-home", default="~/.skcomms", type=click.Path(), help="SKComms home directory.")
|
|
18
18
|
@click.option("--dry-run", is_flag=True, help="Report what would be deleted without deleting.")
|
|
19
|
-
def housekeeping(home: str,
|
|
19
|
+
def housekeeping(home: str, skcomms_home: str, dry_run: bool):
|
|
20
20
|
"""Prune stale ACKs, delivered envelopes, and old seeds.
|
|
21
21
|
|
|
22
22
|
Reclaims disk space from files that accumulate in the agent
|
|
@@ -33,7 +33,7 @@ def register_housekeeping_commands(main: click.Group) -> None:
|
|
|
33
33
|
|
|
34
34
|
results = run_housekeeping(
|
|
35
35
|
skcapstone_home=Path(home).expanduser(),
|
|
36
|
-
|
|
36
|
+
skcomms_home=Path(skcomms_home).expanduser(),
|
|
37
37
|
dry_run=dry_run,
|
|
38
38
|
)
|
|
39
39
|
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""Identity commands: migrate.
|
|
2
|
+
|
|
3
|
+
The ``skcapstone identity migrate`` command backfills every provisioned
|
|
4
|
+
agent's ``identity/identity.json`` with the explicit sovereign-identity
|
|
5
|
+
fields the unified layer expects (skcomms T2 / epic ``2b264064``):
|
|
6
|
+
|
|
7
|
+
* ``realm`` + ``operator`` — mirrored from ``cluster.json``.
|
|
8
|
+
* ``fqid`` — the three-tier ``<agent>@<operator>.<realm>`` label, sourced
|
|
9
|
+
from :func:`capauth.resolve_agent_identity` (the canonical resolver).
|
|
10
|
+
* ``pgp_fingerprint`` — the agent's 40-char PGP fingerprint, also from the
|
|
11
|
+
resolver / the agent's CapAuth profile.
|
|
12
|
+
|
|
13
|
+
This command does **not** reimplement identity logic — it delegates to
|
|
14
|
+
``capauth.resolve_agent_identity`` for the per-agent identity and only mirrors
|
|
15
|
+
``realm``/``operator`` from cluster.json directly (those are cluster facts, not
|
|
16
|
+
agent facts). It is a *walker*: it finds every provisioned agent (one with a
|
|
17
|
+
CapAuth home, never a ``*-template``) and merges the missing fields into its
|
|
18
|
+
identity.json without clobbering unrelated keys.
|
|
19
|
+
|
|
20
|
+
Safety: these are LIVE identity files, so the command defaults to a dry-run
|
|
21
|
+
(it prints a plan and writes nothing). Pass ``--apply`` (alias ``--write``) to
|
|
22
|
+
actually modify files. The operation is idempotent — a second run on an
|
|
23
|
+
already-complete home reports every agent as unchanged.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Optional
|
|
32
|
+
|
|
33
|
+
import click
|
|
34
|
+
|
|
35
|
+
from ._common import SHARED_ROOT, console
|
|
36
|
+
|
|
37
|
+
# Fields the walker backfills, in stable display order.
|
|
38
|
+
_MANAGED_FIELDS = ("realm", "operator", "fqid", "pgp_fingerprint")
|
|
39
|
+
|
|
40
|
+
# cluster.json search path (mirrors capauth.agent_identity._CLUSTER_LOOKUP so
|
|
41
|
+
# realm/operator come from the same source the resolver uses for the fqid).
|
|
42
|
+
_CLUSTER_LOOKUP = [
|
|
43
|
+
Path("/etc/skcapstone/cluster.json"),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class AgentPlan:
|
|
49
|
+
"""Planned identity.json changes for a single agent.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
agent: Short agent name.
|
|
53
|
+
path: Path to the agent's identity/identity.json.
|
|
54
|
+
additions: Field → value mapping that would be written. Empty when
|
|
55
|
+
the agent is already complete (nothing to add).
|
|
56
|
+
applied: Whether the additions were actually written to disk.
|
|
57
|
+
error: Non-empty when the agent could not be processed (e.g. an
|
|
58
|
+
unreadable identity.json); such agents are skipped, not crashed.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
agent: str
|
|
62
|
+
path: Path
|
|
63
|
+
additions: dict[str, str] = field(default_factory=dict)
|
|
64
|
+
applied: bool = False
|
|
65
|
+
error: str = ""
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def changed(self) -> bool:
|
|
69
|
+
"""True when this agent has at least one field to add."""
|
|
70
|
+
return bool(self.additions)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class MigrationPlan:
|
|
75
|
+
"""Aggregate plan across every walked agent.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
home: Shared root that was walked (``~/.skcapstone``).
|
|
79
|
+
dry_run: True when nothing was written to disk.
|
|
80
|
+
cluster_found: Whether a cluster.json was located (realm/operator are
|
|
81
|
+
unavailable when False).
|
|
82
|
+
agents: Per-agent plans (one per provisioned, non-template agent).
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
home: Path
|
|
86
|
+
dry_run: bool
|
|
87
|
+
cluster_found: bool
|
|
88
|
+
agents: list[AgentPlan] = field(default_factory=list)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def changed_count(self) -> int:
|
|
92
|
+
"""Number of agents with at least one field to add."""
|
|
93
|
+
return sum(1 for a in self.agents if a.changed)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def unchanged_count(self) -> int:
|
|
97
|
+
"""Number of already-complete agents (no additions, no error)."""
|
|
98
|
+
return sum(1 for a in self.agents if not a.changed and not a.error)
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> dict:
|
|
101
|
+
"""Serialise to a JSON-friendly dict."""
|
|
102
|
+
return {
|
|
103
|
+
"home": str(self.home),
|
|
104
|
+
"dry_run": self.dry_run,
|
|
105
|
+
"cluster_found": self.cluster_found,
|
|
106
|
+
"changed": self.changed_count,
|
|
107
|
+
"unchanged": self.unchanged_count,
|
|
108
|
+
"agents": [
|
|
109
|
+
{
|
|
110
|
+
"agent": a.agent,
|
|
111
|
+
"path": str(a.path),
|
|
112
|
+
"additions": a.additions,
|
|
113
|
+
"applied": a.applied,
|
|
114
|
+
"error": a.error,
|
|
115
|
+
}
|
|
116
|
+
for a in self.agents
|
|
117
|
+
],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _load_cluster(home: Path) -> Optional[dict]:
|
|
122
|
+
"""Load cluster.json from ``/etc/skcapstone`` then the agent home.
|
|
123
|
+
|
|
124
|
+
Mirrors :data:`capauth.agent_identity._CLUSTER_LOOKUP` but resolves the
|
|
125
|
+
home-local copy relative to *home* so a test (or alternate root) reads the
|
|
126
|
+
fixture cluster.json rather than the real ``~/.skcapstone`` one.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
home: Shared root directory being walked.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The parsed cluster dict, or ``None`` when no cluster.json exists or it
|
|
133
|
+
cannot be parsed.
|
|
134
|
+
"""
|
|
135
|
+
for path in [*_CLUSTER_LOOKUP, home / "cluster.json"]:
|
|
136
|
+
if path.exists():
|
|
137
|
+
try:
|
|
138
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
139
|
+
except (json.JSONDecodeError, OSError):
|
|
140
|
+
continue
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _provisioned_agents(home: Path) -> list[str]:
|
|
145
|
+
"""List agents with a CapAuth home (and thus a real identity).
|
|
146
|
+
|
|
147
|
+
Reuses the "provisioned agent" notion from
|
|
148
|
+
:func:`skcapstone.doctor._provisioned_agents`: an agent counts only when
|
|
149
|
+
``agents/<name>/capauth/`` exists, and ``*-template`` scaffolds are
|
|
150
|
+
excluded.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
home: Shared root directory (``~/.skcapstone``).
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Sorted provisioned agent names.
|
|
157
|
+
"""
|
|
158
|
+
from ..doctor import _provisioned_agents as _doctor_provisioned
|
|
159
|
+
|
|
160
|
+
return _doctor_provisioned(home)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _plan_agent(home: Path, agent: str, cluster: Optional[dict]) -> AgentPlan:
|
|
164
|
+
"""Compute the identity.json additions for one agent.
|
|
165
|
+
|
|
166
|
+
Reads the agent's current ``identity/identity.json`` and determines which
|
|
167
|
+
of ``realm``/``operator``/``fqid``/``pgp_fingerprint`` are missing, using
|
|
168
|
+
cluster.json (realm/operator) and ``capauth.resolve_agent_identity`` (fqid
|
|
169
|
+
+ fingerprint) as the source of truth. Existing values are never
|
|
170
|
+
overwritten.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
home: Shared root directory.
|
|
174
|
+
agent: Short agent name.
|
|
175
|
+
cluster: Parsed cluster.json dict, or ``None``.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
An :class:`AgentPlan` describing the additions (empty when complete or
|
|
179
|
+
when no source value is available), or carrying an ``error`` when the
|
|
180
|
+
identity.json is unreadable.
|
|
181
|
+
"""
|
|
182
|
+
path = home / "agents" / agent / "identity" / "identity.json"
|
|
183
|
+
plan = AgentPlan(agent=agent, path=path)
|
|
184
|
+
|
|
185
|
+
existing: dict = {}
|
|
186
|
+
if path.exists():
|
|
187
|
+
try:
|
|
188
|
+
loaded = json.loads(path.read_text(encoding="utf-8"))
|
|
189
|
+
if isinstance(loaded, dict):
|
|
190
|
+
existing = loaded
|
|
191
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
192
|
+
plan.error = f"unreadable identity.json: {exc}"
|
|
193
|
+
return plan
|
|
194
|
+
|
|
195
|
+
# realm / operator come straight from cluster.json (cluster facts).
|
|
196
|
+
desired: dict[str, Optional[str]] = {}
|
|
197
|
+
if cluster is not None:
|
|
198
|
+
desired["realm"] = cluster.get("realm")
|
|
199
|
+
desired["operator"] = cluster.get("operator")
|
|
200
|
+
|
|
201
|
+
# fqid + pgp_fingerprint come from the canonical resolver — never
|
|
202
|
+
# reimplemented here (epic 2b264064; capauth is the source of truth).
|
|
203
|
+
try:
|
|
204
|
+
from capauth import resolve_agent_identity
|
|
205
|
+
|
|
206
|
+
ident = resolve_agent_identity(agent)
|
|
207
|
+
desired["fqid"] = getattr(ident, "fqid", None)
|
|
208
|
+
desired["pgp_fingerprint"] = getattr(ident, "fingerprint", None)
|
|
209
|
+
except Exception: # noqa: BLE001 — resolver failure must not crash the walk
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
for key in _MANAGED_FIELDS:
|
|
213
|
+
value = desired.get(key)
|
|
214
|
+
# Only add when we have a real value AND it is not already present.
|
|
215
|
+
if value and not existing.get(key):
|
|
216
|
+
plan.additions[key] = str(value)
|
|
217
|
+
|
|
218
|
+
return plan
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def migrate_identities(home: Path, *, apply: bool = False) -> MigrationPlan:
|
|
222
|
+
"""Walk provisioned agents and backfill their identity.json.
|
|
223
|
+
|
|
224
|
+
For every provisioned agent (one with a CapAuth home, never a
|
|
225
|
+
``*-template``), ensure its ``identity/identity.json`` carries ``realm``,
|
|
226
|
+
``operator``, ``fqid`` and ``pgp_fingerprint``. Missing fields are merged
|
|
227
|
+
in without clobbering unrelated keys; files are only written when something
|
|
228
|
+
actually changed.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
home: Shared root directory (``~/.skcapstone``).
|
|
232
|
+
apply: When ``True``, write the changes to disk. When ``False`` (the
|
|
233
|
+
default — these are live files), nothing is written and the
|
|
234
|
+
returned plan is a preview only.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
A :class:`MigrationPlan` describing per-agent additions and whether
|
|
238
|
+
each was applied.
|
|
239
|
+
|
|
240
|
+
Examples:
|
|
241
|
+
>>> plan = migrate_identities(Path("~/.skcapstone").expanduser())
|
|
242
|
+
>>> plan.dry_run
|
|
243
|
+
True
|
|
244
|
+
"""
|
|
245
|
+
cluster = _load_cluster(home)
|
|
246
|
+
plan = MigrationPlan(
|
|
247
|
+
home=home,
|
|
248
|
+
dry_run=not apply,
|
|
249
|
+
cluster_found=cluster is not None,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
for agent in _provisioned_agents(home):
|
|
253
|
+
agent_plan = _plan_agent(home, agent, cluster)
|
|
254
|
+
if apply and agent_plan.changed and not agent_plan.error:
|
|
255
|
+
try:
|
|
256
|
+
_apply_additions(agent_plan)
|
|
257
|
+
agent_plan.applied = True
|
|
258
|
+
except OSError as exc:
|
|
259
|
+
agent_plan.error = f"write failed: {exc}"
|
|
260
|
+
plan.agents.append(agent_plan)
|
|
261
|
+
|
|
262
|
+
return plan
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _apply_additions(plan: AgentPlan) -> None:
|
|
266
|
+
"""Merge a plan's additions into its identity.json on disk.
|
|
267
|
+
|
|
268
|
+
Reads the current file (or starts from ``{}`` if absent), updates only the
|
|
269
|
+
planned keys, and writes the result back with stable indentation. Unrelated
|
|
270
|
+
keys are preserved.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
plan: The agent plan whose ``additions`` should be persisted.
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
OSError: If the file cannot be read or written.
|
|
277
|
+
"""
|
|
278
|
+
data: dict = {}
|
|
279
|
+
if plan.path.exists():
|
|
280
|
+
try:
|
|
281
|
+
loaded = json.loads(plan.path.read_text(encoding="utf-8"))
|
|
282
|
+
if isinstance(loaded, dict):
|
|
283
|
+
data = loaded
|
|
284
|
+
except json.JSONDecodeError:
|
|
285
|
+
data = {}
|
|
286
|
+
data.update(plan.additions)
|
|
287
|
+
plan.path.parent.mkdir(parents=True, exist_ok=True)
|
|
288
|
+
plan.path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def register_identity_commands(main: click.Group) -> None:
|
|
292
|
+
"""Register the ``identity`` command group on the main CLI."""
|
|
293
|
+
|
|
294
|
+
@main.group()
|
|
295
|
+
def identity():
|
|
296
|
+
"""Identity management — migrate per-agent identity.json files."""
|
|
297
|
+
|
|
298
|
+
@identity.command("migrate")
|
|
299
|
+
@click.option(
|
|
300
|
+
"--home", default=SHARED_ROOT, type=click.Path(),
|
|
301
|
+
help="Shared root directory (~/.skcapstone).",
|
|
302
|
+
)
|
|
303
|
+
@click.option(
|
|
304
|
+
"--apply", "--write", "apply_", is_flag=True,
|
|
305
|
+
help="Actually write changes. Default is a dry-run (writes nothing).",
|
|
306
|
+
)
|
|
307
|
+
@click.option(
|
|
308
|
+
"--dry-run", is_flag=True,
|
|
309
|
+
help="Explicitly preview only (the default). Overrides --apply if both given.",
|
|
310
|
+
)
|
|
311
|
+
@click.option("--json-out", is_flag=True, help="Output as machine-readable JSON.")
|
|
312
|
+
def migrate(home: str, apply_: bool, dry_run: bool, json_out: bool) -> None:
|
|
313
|
+
"""Backfill realm/operator/fqid/pgp_fingerprint into agent identity.json.
|
|
314
|
+
|
|
315
|
+
Walks every provisioned agent (one with a CapAuth home, excluding
|
|
316
|
+
``*-template`` dirs) under ``~/.skcapstone/agents/`` and ensures each
|
|
317
|
+
agent's ``identity/identity.json`` carries the explicit sovereign
|
|
318
|
+
fields. Delegates to ``capauth.resolve_agent_identity`` for the fqid
|
|
319
|
+
and fingerprint; realm/operator are mirrored from cluster.json.
|
|
320
|
+
|
|
321
|
+
SAFETY: defaults to a dry-run (prints a plan, writes nothing). Pass
|
|
322
|
+
``--apply`` (or ``--write``) to actually modify the live identity
|
|
323
|
+
files. Idempotent — re-running on a complete home changes nothing.
|
|
324
|
+
"""
|
|
325
|
+
home_path = Path(home).expanduser()
|
|
326
|
+
do_apply = apply_ and not dry_run
|
|
327
|
+
plan = migrate_identities(home_path, apply=do_apply)
|
|
328
|
+
|
|
329
|
+
if json_out:
|
|
330
|
+
click.echo(json.dumps(plan.to_dict(), indent=2))
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
_render_plan(plan)
|
|
334
|
+
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _render_plan(plan: MigrationPlan) -> None:
|
|
339
|
+
"""Render a migration plan as human-readable Rich output."""
|
|
340
|
+
mode = "[yellow]DRY-RUN[/] (no files written — pass --apply to write)" \
|
|
341
|
+
if plan.dry_run else "[green]APPLY[/] (files written)"
|
|
342
|
+
console.print()
|
|
343
|
+
console.print(f" [bold]identity migrate[/] {mode}")
|
|
344
|
+
console.print(f" [dim]{plan.home}[/]")
|
|
345
|
+
if not plan.cluster_found:
|
|
346
|
+
console.print(
|
|
347
|
+
" [yellow]~ cluster.json not found — realm/operator unavailable, "
|
|
348
|
+
"fqid may be incomplete[/]"
|
|
349
|
+
)
|
|
350
|
+
console.print()
|
|
351
|
+
|
|
352
|
+
if not plan.agents:
|
|
353
|
+
console.print(" [dim]No provisioned agents found (none with a CapAuth home).[/]")
|
|
354
|
+
console.print()
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
for a in plan.agents:
|
|
358
|
+
if a.error:
|
|
359
|
+
console.print(f" [red]✗ {a.agent}[/] {a.error}")
|
|
360
|
+
elif not a.changed:
|
|
361
|
+
console.print(f" [green]✓ {a.agent}[/] [dim]unchanged (already complete)[/]")
|
|
362
|
+
else:
|
|
363
|
+
verb = "added" if a.applied else "would add"
|
|
364
|
+
fields = ", ".join(f"{k}={v}" for k, v in a.additions.items())
|
|
365
|
+
color = "green" if a.applied else "cyan"
|
|
366
|
+
console.print(f" [{color}]→ {a.agent}[/] {verb}: {fields}")
|
|
367
|
+
console.print(f" [dim]{a.path}[/]")
|
|
368
|
+
|
|
369
|
+
console.print()
|
|
370
|
+
summary = (
|
|
371
|
+
f" {plan.changed_count} to change, {plan.unchanged_count} unchanged"
|
|
372
|
+
if plan.dry_run
|
|
373
|
+
else f" {plan.changed_count} changed, {plan.unchanged_count} unchanged"
|
|
374
|
+
)
|
|
375
|
+
console.print(f"[bold]{summary}[/]")
|
|
376
|
+
if plan.dry_run and plan.changed_count:
|
|
377
|
+
console.print(" [dim]Re-run with --apply to write these changes.[/]")
|
|
378
|
+
console.print()
|
|
@@ -439,7 +439,7 @@ def register_joule_commands(main: click.Group) -> None:
|
|
|
439
439
|
@joule_group.command("dashboard")
|
|
440
440
|
@click.option(
|
|
441
441
|
"--agent", "-a", "agent_name", default=None,
|
|
442
|
-
help="Agent name (default:
|
|
442
|
+
help="Agent name (default: current agent).",
|
|
443
443
|
)
|
|
444
444
|
def dashboard_cmd(agent_name: str | None):
|
|
445
445
|
"""Show a financial dashboard for an agent."""
|
|
@@ -451,7 +451,9 @@ def register_joule_commands(main: click.Group) -> None:
|
|
|
451
451
|
|
|
452
452
|
from ..skjoule import JouleEngine, TransactionKind
|
|
453
453
|
|
|
454
|
-
|
|
454
|
+
from .. import active_agent_name
|
|
455
|
+
|
|
456
|
+
agent_name = agent_name or active_agent_name()
|
|
455
457
|
engine = JouleEngine(home=Path(SHARED_ROOT).expanduser())
|
|
456
458
|
wallet = engine.get_wallet(agent_name)
|
|
457
459
|
balance = wallet.balance
|
|
@@ -624,4 +626,6 @@ def _resolve_agent(agent_name: str | None) -> str:
|
|
|
624
626
|
if agent_name:
|
|
625
627
|
return agent_name
|
|
626
628
|
from .. import SKCAPSTONE_AGENT
|
|
627
|
-
|
|
629
|
+
from .. import active_agent_name
|
|
630
|
+
|
|
631
|
+
return SKCAPSTONE_AGENT or active_agent_name() or ""
|