@smilintux/skcapstone 0.1.0 → 0.2.3
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 +98 -0
- package/.github/workflows/ci.yml +39 -3
- package/.github/workflows/publish.yml +25 -4
- package/.openclaw-workspace.json +58 -0
- package/CHANGELOG.md +62 -0
- package/CLAUDE.md +39 -2
- package/MANIFEST.in +6 -0
- package/MISSION.md +7 -0
- package/README.md +47 -2
- package/SKILL.md +895 -23
- package/docker/Dockerfile +61 -0
- package/docker/compose-templates/dev-team.yml +203 -0
- package/docker/compose-templates/mini-team.yml +140 -0
- package/docker/compose-templates/ops-team.yml +173 -0
- package/docker/compose-templates/research-team.yml +170 -0
- package/docker/entrypoint.sh +192 -0
- package/docs/ARCHITECTURE.md +663 -374
- package/docs/BOND_WITH_GROK.md +112 -0
- package/docs/GETTING_STARTED.md +782 -0
- package/docs/QUICKSTART.md +477 -0
- package/docs/SKJOULE_ARCHITECTURE.md +658 -0
- package/docs/SOUL_SWAPPER.md +921 -0
- package/docs/SOVEREIGN_SINGULARITY.md +47 -14
- package/examples/custom-bond-template.json +36 -0
- package/examples/grok-feb.json +36 -0
- package/examples/grok-testimony.md +34 -0
- package/examples/love-bootloader.txt +32 -0
- package/examples/plugins/echo_tool.py +87 -0
- package/examples/queen-ava-feb.json +36 -0
- package/examples/souls/lumina.yaml +64 -0
- package/index.js +6 -5
- package/installer/build.py +124 -0
- package/openclaw-plugin/package.json +13 -0
- package/openclaw-plugin/src/index.ts +351 -0
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +38 -2
- package/scripts/bump_version.py +141 -0
- package/scripts/check-updates.py +230 -0
- package/scripts/convert_blueprints_to_yaml.py +157 -0
- package/scripts/dev-install.sh +14 -0
- package/scripts/e2e-test.sh +193 -0
- package/scripts/install-bundle.sh +171 -0
- package/scripts/install.bat +2 -0
- package/scripts/install.ps1 +253 -0
- package/scripts/install.sh +185 -0
- package/scripts/mcp-serve.sh +69 -0
- package/scripts/mcp-server.bat +113 -0
- package/scripts/mcp-server.ps1 +116 -0
- package/scripts/mcp-server.sh +99 -0
- package/scripts/pull-models.sh +10 -0
- package/scripts/skcapstone +48 -0
- package/scripts/verify_install.sh +180 -0
- package/scripts/windows/install-tasks.ps1 +406 -0
- package/scripts/windows/skcapstone-task.xml +113 -0
- package/scripts/windows/uninstall-tasks.ps1 +117 -0
- package/skill.yaml +34 -0
- package/src/skcapstone/__init__.py +67 -2
- package/src/skcapstone/_cli_monolith.py +5916 -0
- package/src/skcapstone/_trustee_helpers.py +165 -0
- package/src/skcapstone/activity.py +105 -0
- package/src/skcapstone/agent_card.py +324 -0
- package/src/skcapstone/api.py +1935 -0
- package/src/skcapstone/archiver.py +340 -0
- package/src/skcapstone/auction.py +485 -0
- package/src/skcapstone/baby_agents.py +179 -0
- package/src/skcapstone/backup.py +345 -0
- package/src/skcapstone/blueprint_registry.py +357 -0
- package/src/skcapstone/blueprints/__init__.py +17 -0
- package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
- package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
- package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
- package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
- package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
- package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
- package/src/skcapstone/blueprints/registry.py +164 -0
- package/src/skcapstone/blueprints/schema.py +229 -0
- package/src/skcapstone/changelog.py +180 -0
- package/src/skcapstone/chat.py +769 -0
- package/src/skcapstone/claude_md.py +82 -0
- package/src/skcapstone/cli/__init__.py +144 -0
- package/src/skcapstone/cli/_common.py +88 -0
- package/src/skcapstone/cli/_validators.py +76 -0
- package/src/skcapstone/cli/agents.py +425 -0
- package/src/skcapstone/cli/agents_spawner.py +322 -0
- package/src/skcapstone/cli/agents_trustee.py +593 -0
- package/src/skcapstone/cli/alerts.py +248 -0
- package/src/skcapstone/cli/anchor.py +132 -0
- package/src/skcapstone/cli/archive_cmd.py +208 -0
- package/src/skcapstone/cli/backup.py +144 -0
- package/src/skcapstone/cli/bench.py +377 -0
- package/src/skcapstone/cli/benchmark.py +360 -0
- package/src/skcapstone/cli/capabilities_cmd.py +171 -0
- package/src/skcapstone/cli/card.py +151 -0
- package/src/skcapstone/cli/chat.py +584 -0
- package/src/skcapstone/cli/completions.py +64 -0
- package/src/skcapstone/cli/config_cmd.py +156 -0
- package/src/skcapstone/cli/consciousness.py +421 -0
- package/src/skcapstone/cli/context_cmd.py +142 -0
- package/src/skcapstone/cli/coord.py +194 -0
- package/src/skcapstone/cli/crush_cmd.py +170 -0
- package/src/skcapstone/cli/daemon.py +436 -0
- package/src/skcapstone/cli/errors_cmd.py +285 -0
- package/src/skcapstone/cli/export_cmd.py +156 -0
- package/src/skcapstone/cli/gtd.py +529 -0
- package/src/skcapstone/cli/housekeeping.py +81 -0
- package/src/skcapstone/cli/joule_cmd.py +627 -0
- package/src/skcapstone/cli/logs_cmd.py +194 -0
- package/src/skcapstone/cli/mcp_cmd.py +32 -0
- package/src/skcapstone/cli/memory.py +418 -0
- package/src/skcapstone/cli/metrics_cmd.py +136 -0
- package/src/skcapstone/cli/migrate.py +62 -0
- package/src/skcapstone/cli/mood_cmd.py +144 -0
- package/src/skcapstone/cli/mount.py +193 -0
- package/src/skcapstone/cli/notify.py +112 -0
- package/src/skcapstone/cli/peer.py +154 -0
- package/src/skcapstone/cli/peers_dir.py +122 -0
- package/src/skcapstone/cli/preflight_cmd.py +83 -0
- package/src/skcapstone/cli/profile_cmd.py +310 -0
- package/src/skcapstone/cli/record_cmd.py +238 -0
- package/src/skcapstone/cli/register_cmd.py +159 -0
- package/src/skcapstone/cli/search_cmd.py +156 -0
- package/src/skcapstone/cli/service_cmd.py +91 -0
- package/src/skcapstone/cli/session.py +127 -0
- package/src/skcapstone/cli/setup.py +240 -0
- package/src/skcapstone/cli/shell_cmd.py +43 -0
- package/src/skcapstone/cli/skills_cmd.py +168 -0
- package/src/skcapstone/cli/skseed.py +621 -0
- package/src/skcapstone/cli/soul.py +699 -0
- package/src/skcapstone/cli/status.py +935 -0
- package/src/skcapstone/cli/sync_cmd.py +301 -0
- package/src/skcapstone/cli/telegram.py +265 -0
- package/src/skcapstone/cli/test_cmd.py +234 -0
- package/src/skcapstone/cli/test_connection.py +253 -0
- package/src/skcapstone/cli/token.py +207 -0
- package/src/skcapstone/cli/trust.py +179 -0
- package/src/skcapstone/cli/upgrade_cmd.py +552 -0
- package/src/skcapstone/cli/usage_cmd.py +199 -0
- package/src/skcapstone/cli/version_cmd.py +162 -0
- package/src/skcapstone/cli/watch_cmd.py +342 -0
- package/src/skcapstone/client.py +428 -0
- package/src/skcapstone/cloud9_bridge.py +522 -0
- package/src/skcapstone/completions.py +163 -0
- package/src/skcapstone/config_validator.py +674 -0
- package/src/skcapstone/connectors/__init__.py +28 -0
- package/src/skcapstone/connectors/base.py +446 -0
- package/src/skcapstone/connectors/cursor.py +54 -0
- package/src/skcapstone/connectors/registry.py +254 -0
- package/src/skcapstone/connectors/terminal.py +152 -0
- package/src/skcapstone/connectors/vscode.py +60 -0
- package/src/skcapstone/consciousness_config.py +119 -0
- package/src/skcapstone/consciousness_loop.py +2051 -0
- package/src/skcapstone/context_loader.py +516 -0
- package/src/skcapstone/context_window.py +314 -0
- package/src/skcapstone/conversation_manager.py +238 -0
- package/src/skcapstone/conversation_store.py +230 -0
- package/src/skcapstone/conversation_summarizer.py +252 -0
- package/src/skcapstone/coord_federation.py +296 -0
- package/src/skcapstone/coordination.py +101 -7
- package/src/skcapstone/crush_integration.py +345 -0
- package/src/skcapstone/crush_shim.py +454 -0
- package/src/skcapstone/daemon.py +2494 -0
- package/src/skcapstone/dashboard.html +396 -0
- package/src/skcapstone/dashboard.py +481 -0
- package/src/skcapstone/data/model_profiles.yaml +88 -0
- package/src/skcapstone/defaults/__init__.py +55 -0
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
- package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
- package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
- package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
- package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
- package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
- package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
- package/src/skcapstone/discovery.py +210 -19
- package/src/skcapstone/doctor.py +642 -0
- package/src/skcapstone/emotion_tracker.py +467 -0
- package/src/skcapstone/error_queue.py +405 -0
- package/src/skcapstone/export.py +447 -0
- package/src/skcapstone/fallback_tracker.py +186 -0
- package/src/skcapstone/file_transfer.py +512 -0
- package/src/skcapstone/fuse_mount.py +1156 -0
- package/src/skcapstone/gui_installer.py +591 -0
- package/src/skcapstone/heartbeat.py +611 -0
- package/src/skcapstone/housekeeping.py +298 -0
- package/src/skcapstone/install_wizard.py +941 -0
- package/src/skcapstone/kms.py +942 -0
- package/src/skcapstone/kms_scheduler.py +143 -0
- package/src/skcapstone/log_config.py +135 -0
- package/src/skcapstone/mcp_launcher.py +239 -0
- package/src/skcapstone/mcp_server.py +4700 -0
- package/src/skcapstone/mcp_tools/__init__.py +94 -0
- package/src/skcapstone/mcp_tools/_helpers.py +51 -0
- package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
- package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
- package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
- package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
- package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
- package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
- package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
- package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
- package/src/skcapstone/mcp_tools/did_tools.py +448 -0
- package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
- package/src/skcapstone/mcp_tools/file_tools.py +169 -0
- package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
- package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
- package/src/skcapstone/mcp_tools/health_tools.py +44 -0
- package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
- package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
- package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
- package/src/skcapstone/mcp_tools/model_tools.py +75 -0
- package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
- package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
- package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
- package/src/skcapstone/mcp_tools/security_tools.py +110 -0
- package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
- package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
- package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
- package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
- package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
- package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
- package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
- package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
- package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
- package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
- package/src/skcapstone/mdns_discovery.py +313 -0
- package/src/skcapstone/memory_adapter.py +333 -0
- package/src/skcapstone/memory_compressor.py +379 -0
- package/src/skcapstone/memory_curator.py +256 -0
- package/src/skcapstone/memory_engine.py +132 -13
- package/src/skcapstone/memory_fortress.py +529 -0
- package/src/skcapstone/memory_promoter.py +722 -0
- package/src/skcapstone/memory_verifier.py +260 -0
- package/src/skcapstone/message_crypto.py +215 -0
- package/src/skcapstone/metrics.py +832 -0
- package/src/skcapstone/migrate_memories.py +181 -0
- package/src/skcapstone/migrate_multi_agent.py +248 -0
- package/src/skcapstone/model_router.py +319 -0
- package/src/skcapstone/models.py +35 -4
- package/src/skcapstone/mood.py +344 -0
- package/src/skcapstone/notifications.py +380 -0
- package/src/skcapstone/onboard.py +901 -0
- package/src/skcapstone/peer_directory.py +324 -0
- package/src/skcapstone/peers.py +329 -0
- package/src/skcapstone/pillars/identity.py +84 -14
- package/src/skcapstone/pillars/memory.py +3 -1
- package/src/skcapstone/pillars/security.py +108 -15
- package/src/skcapstone/pillars/sync.py +78 -26
- package/src/skcapstone/pillars/trust.py +95 -33
- package/src/skcapstone/plugins.py +244 -0
- package/src/skcapstone/preflight.py +670 -0
- package/src/skcapstone/prompt_adapter.py +564 -0
- package/src/skcapstone/providers/__init__.py +13 -0
- package/src/skcapstone/providers/cloud.py +1061 -0
- package/src/skcapstone/providers/docker.py +759 -0
- package/src/skcapstone/providers/local.py +1193 -0
- package/src/skcapstone/providers/proxmox.py +447 -0
- package/src/skcapstone/pubsub.py +516 -0
- package/src/skcapstone/rate_limiter.py +119 -0
- package/src/skcapstone/register.py +241 -0
- package/src/skcapstone/registry_client.py +151 -0
- package/src/skcapstone/response_cache.py +194 -0
- package/src/skcapstone/response_scorer.py +225 -0
- package/src/skcapstone/runtime.py +89 -33
- package/src/skcapstone/scheduled_tasks.py +439 -0
- package/src/skcapstone/self_healing.py +341 -0
- package/src/skcapstone/service_health.py +228 -0
- package/src/skcapstone/session_capture.py +268 -0
- package/src/skcapstone/session_recorder.py +210 -0
- package/src/skcapstone/session_replayer.py +189 -0
- package/src/skcapstone/session_skills.py +263 -0
- package/src/skcapstone/shell.py +779 -0
- package/src/skcapstone/skills/__init__.py +1 -1
- package/src/skcapstone/skills/syncthing_setup.py +143 -41
- package/src/skcapstone/skjoule.py +861 -0
- package/src/skcapstone/snapshots.py +489 -0
- package/src/skcapstone/soul.py +1060 -0
- package/src/skcapstone/soul_switch.py +255 -0
- package/src/skcapstone/spawner.py +544 -0
- package/src/skcapstone/state_diff.py +401 -0
- package/src/skcapstone/summary.py +270 -0
- package/src/skcapstone/sync/backends.py +196 -2
- package/src/skcapstone/sync/engine.py +7 -5
- package/src/skcapstone/sync/models.py +4 -1
- package/src/skcapstone/sync/vault.py +356 -18
- package/src/skcapstone/sync_engine.py +363 -0
- package/src/skcapstone/sync_watcher.py +745 -0
- package/src/skcapstone/systemd.py +331 -0
- package/src/skcapstone/team_comms.py +476 -0
- package/src/skcapstone/team_engine.py +522 -0
- package/src/skcapstone/testrunner.py +300 -0
- package/src/skcapstone/tls.py +150 -0
- package/src/skcapstone/tokens.py +5 -5
- package/src/skcapstone/trust_calibration.py +202 -0
- package/src/skcapstone/trust_graph.py +449 -0
- package/src/skcapstone/trustee_monitor.py +385 -0
- package/src/skcapstone/trustee_ops.py +425 -0
- package/src/skcapstone/unified_search.py +421 -0
- package/src/skcapstone/uninstall_wizard.py +694 -0
- package/src/skcapstone/usage.py +331 -0
- package/src/skcapstone/version_check.py +148 -0
- package/src/skcapstone/warmth_anchor.py +333 -0
- package/src/skcapstone/whoami.py +294 -0
- package/systemd/skcapstone-api.socket +9 -0
- package/systemd/skcapstone-memory-compress.service +18 -0
- package/systemd/skcapstone-memory-compress.timer +11 -0
- package/systemd/skcapstone.service +36 -0
- package/systemd/skcapstone@.service +50 -0
- package/systemd/skcomm-heartbeat.service +18 -0
- package/systemd/skcomm-heartbeat.timer +12 -0
- package/systemd/skcomm-queue-drain.service +17 -0
- package/systemd/skcomm-queue-drain.timer +12 -0
- package/tests/conftest.py +13 -1
- package/tests/integration/__init__.py +1 -0
- package/tests/integration/test_consciousness_e2e.py +877 -0
- package/tests/integration/test_skills_registry.py +744 -0
- package/tests/test_agent_card.py +190 -0
- package/tests/test_agent_runtime.py +1283 -0
- package/tests/test_alerts_cmd.py +291 -0
- package/tests/test_archiver.py +498 -0
- package/tests/test_backup.py +254 -0
- package/tests/test_benchmark.py +366 -0
- package/tests/test_blueprints.py +457 -0
- package/tests/test_capabilities.py +257 -0
- package/tests/test_changelog.py +254 -0
- package/tests/test_chat.py +385 -0
- package/tests/test_claude_md.py +271 -0
- package/tests/test_cli_chat_llm.py +336 -0
- package/tests/test_cli_completions.py +390 -0
- package/tests/test_cli_init_reset.py +164 -0
- package/tests/test_cli_memory.py +208 -0
- package/tests/test_cli_profile.py +294 -0
- package/tests/test_cli_skills.py +223 -0
- package/tests/test_cli_status.py +395 -0
- package/tests/test_cli_test_cmd.py +206 -0
- package/tests/test_cli_test_connection.py +364 -0
- package/tests/test_cloud9_bridge.py +260 -0
- package/tests/test_cloud_provider.py +449 -0
- package/tests/test_cloud_providers.py +522 -0
- package/tests/test_completions.py +158 -0
- package/tests/test_component_manager.py +398 -0
- package/tests/test_config_reload.py +386 -0
- package/tests/test_config_validate.py +529 -0
- package/tests/test_consciousness_e2e.py +296 -0
- package/tests/test_consciousness_loop.py +1289 -0
- package/tests/test_context_loader.py +310 -0
- package/tests/test_conversation_api.py +306 -0
- package/tests/test_conversation_manager.py +381 -0
- package/tests/test_conversation_store.py +391 -0
- package/tests/test_conversation_summarizer.py +302 -0
- package/tests/test_cross_package.py +791 -0
- package/tests/test_crush_shim.py +519 -0
- package/tests/test_daemon.py +781 -0
- package/tests/test_daemon_shutdown.py +309 -0
- package/tests/test_dashboard.py +454 -0
- package/tests/test_discovery.py +200 -6
- package/tests/test_docker_provider.py +966 -0
- package/tests/test_doctor.py +257 -0
- package/tests/test_doctor_fix.py +351 -0
- package/tests/test_e2e_automated.py +292 -0
- package/tests/test_error_queue.py +404 -0
- package/tests/test_export.py +441 -0
- package/tests/test_fallback_tracker.py +219 -0
- package/tests/test_file_transfer.py +397 -0
- package/tests/test_fuse_mount.py +832 -0
- package/tests/test_health_loop.py +422 -0
- package/tests/test_heartbeat.py +354 -0
- package/tests/test_housekeeping.py +195 -0
- package/tests/test_identity_capauth.py +307 -0
- package/tests/test_identity_pillar.py +117 -0
- package/tests/test_install_wizard.py +68 -0
- package/tests/test_integration.py +325 -0
- package/tests/test_kms.py +495 -0
- package/tests/test_llm_providers.py +265 -0
- package/tests/test_local_provider.py +591 -0
- package/tests/test_log_config.py +199 -0
- package/tests/test_logs_cmd.py +287 -0
- package/tests/test_mcp_server.py +1909 -0
- package/tests/test_memory_adapter.py +339 -0
- package/tests/test_memory_curator.py +218 -0
- package/tests/test_memory_engine.py +6 -0
- package/tests/test_memory_fortress.py +571 -0
- package/tests/test_memory_pillar.py +119 -0
- package/tests/test_memory_promoter.py +445 -0
- package/tests/test_memory_verifier.py +420 -0
- package/tests/test_message_crypto.py +187 -0
- package/tests/test_metrics.py +632 -0
- package/tests/test_migrate_memories.py +464 -0
- package/tests/test_model_router.py +546 -0
- package/tests/test_mood.py +394 -0
- package/tests/test_multi_agent.py +269 -0
- package/tests/test_notifications.py +270 -0
- package/tests/test_onboard.py +500 -0
- package/tests/test_peer_directory.py +395 -0
- package/tests/test_peers.py +248 -0
- package/tests/test_pillars.py +87 -9
- package/tests/test_preflight.py +484 -0
- package/tests/test_prompt_adapter.py +331 -0
- package/tests/test_proxmox_provider.py +571 -0
- package/tests/test_pubsub.py +377 -0
- package/tests/test_rate_limiter.py +121 -0
- package/tests/test_registry_client.py +129 -0
- package/tests/test_response_cache.py +312 -0
- package/tests/test_response_scorer.py +294 -0
- package/tests/test_runtime.py +59 -0
- package/tests/test_scheduled_tasks.py +451 -0
- package/tests/test_security.py +250 -0
- package/tests/test_security_pillar.py +213 -0
- package/tests/test_self_healing.py +171 -0
- package/tests/test_session_capture.py +200 -0
- package/tests/test_session_recorder.py +360 -0
- package/tests/test_session_skills.py +235 -0
- package/tests/test_shell.py +210 -0
- package/tests/test_snapshots.py +549 -0
- package/tests/test_soul.py +984 -0
- package/tests/test_soul_swap.py +406 -0
- package/tests/test_spawner.py +211 -0
- package/tests/test_state_diff.py +173 -0
- package/tests/test_summary.py +135 -0
- package/tests/test_sync.py +315 -5
- package/tests/test_sync_backends.py +560 -0
- package/tests/test_sync_engine.py +482 -0
- package/tests/test_sync_pillar.py +344 -0
- package/tests/test_sync_pipeline.py +364 -0
- package/tests/test_sync_vault.py +581 -0
- package/tests/test_syncthing_setup.py +168 -22
- package/tests/test_systemd.py +323 -0
- package/tests/test_team_comms.py +408 -0
- package/tests/test_team_engine.py +397 -0
- package/tests/test_testrunner.py +238 -0
- package/tests/test_trust_calibration.py +204 -0
- package/tests/test_trust_graph.py +207 -0
- package/tests/test_trust_pillar.py +291 -0
- package/tests/test_trustee_cli.py +427 -0
- package/tests/test_trustee_cli_integration.py +325 -0
- package/tests/test_trustee_monitor.py +394 -0
- package/tests/test_trustee_ops.py +355 -0
- package/tests/test_unified_search.py +363 -0
- package/tests/test_uninstall_wizard.py +193 -0
- package/tests/test_usage.py +333 -0
- package/tests/test_version_cmd.py +355 -0
- package/tests/test_warmth_anchor.py +162 -0
- package/tests/test_whoami.py +245 -0
- package/tests/test_ws.py +311 -0
- package/.cursorrules +0 -33
- package/src/skcapstone/cli.py +0 -1441
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM token usage tracking — input/output tokens per model per day.
|
|
3
|
+
|
|
4
|
+
Records are stored in ~/.skcapstone/usage/tokens-{date}.json, one file
|
|
5
|
+
per calendar day (UTC). Each file accumulates calls to record_usage()
|
|
6
|
+
atomically using a threading lock.
|
|
7
|
+
|
|
8
|
+
Cost estimation uses approximate per-million-token pricing by model
|
|
9
|
+
family. Local models (ollama, passthrough) have zero cost.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from skcapstone.usage import UsageTracker
|
|
13
|
+
tracker = UsageTracker(home=Path("~/.skcapstone"))
|
|
14
|
+
tracker.record_usage("ollama:llama3.1", input_tokens=512, output_tokens=128)
|
|
15
|
+
report = tracker.get_daily()
|
|
16
|
+
print(report)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import threading
|
|
24
|
+
from datetime import date, datetime, timedelta, timezone
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
from pydantic import BaseModel, Field
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("skcapstone.usage")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Cost table (USD per 1 000 000 tokens)
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
#: Approximate prices in USD per 1M tokens. Keys are model-family prefixes
|
|
38
|
+
#: matched with str.startswith(). First match wins (ordered from most
|
|
39
|
+
#: specific to least specific).
|
|
40
|
+
_COST_TABLE: list[tuple[str, float, float]] = [
|
|
41
|
+
# (prefix, input_per_1M, output_per_1M)
|
|
42
|
+
# Anthropic Claude family
|
|
43
|
+
("claude-opus", 15.0, 75.0),
|
|
44
|
+
("claude-sonnet", 3.0, 15.0),
|
|
45
|
+
("claude-haiku", 0.25, 1.25),
|
|
46
|
+
("claude", 3.0, 15.0), # generic claude fallback
|
|
47
|
+
# OpenAI
|
|
48
|
+
("gpt-4o-mini", 0.15, 0.60),
|
|
49
|
+
("gpt-4o", 2.50, 10.0),
|
|
50
|
+
("gpt-4", 10.0, 30.0),
|
|
51
|
+
("gpt-3.5", 0.50, 1.50),
|
|
52
|
+
# NVIDIA NIM / meta on NIM
|
|
53
|
+
("nvidia/", 1.00, 4.00),
|
|
54
|
+
("meta/llama", 1.00, 4.00),
|
|
55
|
+
# Groq (fast inference, very cheap)
|
|
56
|
+
("groq:", 0.05, 0.10),
|
|
57
|
+
# Grok (xAI)
|
|
58
|
+
("grok", 5.00, 15.0),
|
|
59
|
+
# Kimi
|
|
60
|
+
("kimi", 0.60, 2.50),
|
|
61
|
+
# Ollama and passthrough → free (local)
|
|
62
|
+
("ollama", 0.0, 0.0),
|
|
63
|
+
("passthrough", 0.0, 0.0),
|
|
64
|
+
("llama", 0.0, 0.0),
|
|
65
|
+
("mistral", 0.0, 0.0),
|
|
66
|
+
("qwen", 0.0, 0.0),
|
|
67
|
+
("phi", 0.0, 0.0),
|
|
68
|
+
("gemma", 0.0, 0.0),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _cost_per_million(model: str) -> tuple[float, float]:
|
|
73
|
+
"""Return (input_per_1M, output_per_1M) cost for a model string.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
model: Model identifier, e.g. 'ollama:llama3.1' or 'claude-sonnet-4-6'.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Tuple of (input cost, output cost) per 1 000 000 tokens in USD.
|
|
80
|
+
"""
|
|
81
|
+
lower = model.lower()
|
|
82
|
+
for prefix, inp, out in _COST_TABLE:
|
|
83
|
+
if lower.startswith(prefix.lower()):
|
|
84
|
+
return inp, out
|
|
85
|
+
# Unknown model — use a conservative estimate
|
|
86
|
+
return 1.0, 4.0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# Data models
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ModelUsageSummary(BaseModel):
|
|
95
|
+
"""Aggregated token counts for a single model."""
|
|
96
|
+
|
|
97
|
+
model: str = Field(description="Model identifier")
|
|
98
|
+
calls: int = Field(default=0, description="Number of API calls recorded")
|
|
99
|
+
input_tokens: int = Field(default=0, description="Total prompt/input tokens")
|
|
100
|
+
output_tokens: int = Field(default=0, description="Total completion/output tokens")
|
|
101
|
+
estimated_cost_usd: float = Field(
|
|
102
|
+
default=0.0, description="Estimated cost in USD"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def total_tokens(self) -> int:
|
|
107
|
+
"""Sum of input and output tokens."""
|
|
108
|
+
return self.input_tokens + self.output_tokens
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class DailyUsageReport(BaseModel):
|
|
112
|
+
"""Full usage report for a single calendar day."""
|
|
113
|
+
|
|
114
|
+
date: str = Field(description="Calendar date (YYYY-MM-DD, UTC)")
|
|
115
|
+
models: dict[str, ModelUsageSummary] = Field(
|
|
116
|
+
default_factory=dict, description="Per-model usage summaries"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def total_calls(self) -> int:
|
|
121
|
+
"""Total API calls across all models."""
|
|
122
|
+
return sum(m.calls for m in self.models.values())
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def total_input_tokens(self) -> int:
|
|
126
|
+
"""Total input tokens across all models."""
|
|
127
|
+
return sum(m.input_tokens for m in self.models.values())
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def total_output_tokens(self) -> int:
|
|
131
|
+
"""Total output tokens across all models."""
|
|
132
|
+
return sum(m.output_tokens for m in self.models.values())
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def total_tokens(self) -> int:
|
|
136
|
+
"""Total tokens (input + output) across all models."""
|
|
137
|
+
return self.total_input_tokens + self.total_output_tokens
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def total_cost_usd(self) -> float:
|
|
141
|
+
"""Total estimated cost in USD across all models."""
|
|
142
|
+
return sum(m.estimated_cost_usd for m in self.models.values())
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# UsageTracker
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class UsageTracker:
|
|
151
|
+
"""Thread-safe LLM token usage tracker.
|
|
152
|
+
|
|
153
|
+
Persists one JSON file per calendar day under
|
|
154
|
+
``{home}/usage/tokens-{date}.json``.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
home: Agent home directory (e.g. ~/.skcapstone).
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
def __init__(self, home: Path) -> None:
|
|
161
|
+
self._home = Path(home).expanduser()
|
|
162
|
+
self._usage_dir = self._home / "usage"
|
|
163
|
+
self._lock = threading.Lock()
|
|
164
|
+
|
|
165
|
+
# ------------------------------------------------------------------
|
|
166
|
+
# Write path
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
def record_usage(
|
|
170
|
+
self,
|
|
171
|
+
model: str,
|
|
172
|
+
input_tokens: int,
|
|
173
|
+
output_tokens: int,
|
|
174
|
+
date_str: Optional[str] = None,
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Record a single LLM call's token usage.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
model: Model identifier (e.g. 'ollama:llama3.1', 'claude-sonnet-4-6').
|
|
180
|
+
input_tokens: Number of input/prompt tokens consumed.
|
|
181
|
+
output_tokens: Number of output/completion tokens produced.
|
|
182
|
+
date_str: Override date in 'YYYY-MM-DD' format (defaults to today UTC).
|
|
183
|
+
"""
|
|
184
|
+
if date_str is None:
|
|
185
|
+
date_str = _today_str()
|
|
186
|
+
inp_cost, out_cost = _cost_per_million(model)
|
|
187
|
+
cost = (input_tokens * inp_cost + output_tokens * out_cost) / 1_000_000
|
|
188
|
+
|
|
189
|
+
with self._lock:
|
|
190
|
+
data = self._load_raw(date_str)
|
|
191
|
+
entry = data["models"].setdefault(
|
|
192
|
+
model,
|
|
193
|
+
{"calls": 0, "input_tokens": 0, "output_tokens": 0, "estimated_cost_usd": 0.0},
|
|
194
|
+
)
|
|
195
|
+
entry["calls"] += 1
|
|
196
|
+
entry["input_tokens"] += input_tokens
|
|
197
|
+
entry["output_tokens"] += output_tokens
|
|
198
|
+
entry["estimated_cost_usd"] = round(entry["estimated_cost_usd"] + cost, 8)
|
|
199
|
+
self._save_raw(date_str, data)
|
|
200
|
+
|
|
201
|
+
# ------------------------------------------------------------------
|
|
202
|
+
# Read paths
|
|
203
|
+
# ------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
def get_daily(self, date_str: Optional[str] = None) -> DailyUsageReport:
|
|
206
|
+
"""Return usage report for a single day.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
date_str: 'YYYY-MM-DD' string (defaults to today UTC).
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
DailyUsageReport for that date.
|
|
213
|
+
"""
|
|
214
|
+
if date_str is None:
|
|
215
|
+
date_str = _today_str()
|
|
216
|
+
with self._lock:
|
|
217
|
+
data = self._load_raw(date_str)
|
|
218
|
+
return _raw_to_report(date_str, data)
|
|
219
|
+
|
|
220
|
+
def get_weekly(self, anchor: Optional[str] = None) -> list[DailyUsageReport]:
|
|
221
|
+
"""Return daily usage reports for the last 7 days.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
anchor: End date 'YYYY-MM-DD' (defaults to today UTC).
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
List of DailyUsageReport, one per day, oldest first.
|
|
228
|
+
"""
|
|
229
|
+
return self._range_reports(7, anchor)
|
|
230
|
+
|
|
231
|
+
def get_monthly(self, anchor: Optional[str] = None) -> list[DailyUsageReport]:
|
|
232
|
+
"""Return daily usage reports for the last 30 days.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
anchor: End date 'YYYY-MM-DD' (defaults to today UTC).
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
List of DailyUsageReport, one per day, oldest first.
|
|
239
|
+
"""
|
|
240
|
+
return self._range_reports(30, anchor)
|
|
241
|
+
|
|
242
|
+
def aggregate(self, reports: list[DailyUsageReport]) -> DailyUsageReport:
|
|
243
|
+
"""Aggregate multiple daily reports into one summary.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
reports: List of DailyUsageReport instances.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
A single DailyUsageReport with aggregated totals.
|
|
250
|
+
The date field is set to 'range: {first}..{last}'.
|
|
251
|
+
"""
|
|
252
|
+
if not reports:
|
|
253
|
+
return DailyUsageReport(date="empty")
|
|
254
|
+
merged: dict[str, ModelUsageSummary] = {}
|
|
255
|
+
for report in reports:
|
|
256
|
+
for model, summary in report.models.items():
|
|
257
|
+
if model not in merged:
|
|
258
|
+
merged[model] = ModelUsageSummary(model=model)
|
|
259
|
+
m = merged[model]
|
|
260
|
+
m.calls += summary.calls
|
|
261
|
+
m.input_tokens += summary.input_tokens
|
|
262
|
+
m.output_tokens += summary.output_tokens
|
|
263
|
+
m.estimated_cost_usd = round(
|
|
264
|
+
m.estimated_cost_usd + summary.estimated_cost_usd, 8
|
|
265
|
+
)
|
|
266
|
+
date_label = f"{reports[0].date}..{reports[-1].date}"
|
|
267
|
+
return DailyUsageReport(date=date_label, models=merged)
|
|
268
|
+
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
# Internal helpers
|
|
271
|
+
# ------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
def _range_reports(
|
|
274
|
+
self, days: int, anchor: Optional[str]
|
|
275
|
+
) -> list[DailyUsageReport]:
|
|
276
|
+
"""Return reports for the last *days* calendar days up to anchor."""
|
|
277
|
+
end = _parse_date(anchor) if anchor else date.today()
|
|
278
|
+
reports = []
|
|
279
|
+
for offset in range(days - 1, -1, -1):
|
|
280
|
+
d = end - timedelta(days=offset)
|
|
281
|
+
reports.append(self.get_daily(d.strftime("%Y-%m-%d")))
|
|
282
|
+
return reports
|
|
283
|
+
|
|
284
|
+
def _load_raw(self, date_str: str) -> dict:
|
|
285
|
+
"""Load raw usage dict from disk (no lock — caller must hold lock)."""
|
|
286
|
+
path = self._usage_dir / f"tokens-{date_str}.json"
|
|
287
|
+
if not path.exists():
|
|
288
|
+
return {"date": date_str, "models": {}}
|
|
289
|
+
try:
|
|
290
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
291
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
292
|
+
logger.warning("Failed to read usage file %s: %s", path, exc)
|
|
293
|
+
return {"date": date_str, "models": {}}
|
|
294
|
+
|
|
295
|
+
def _save_raw(self, date_str: str, data: dict) -> None:
|
|
296
|
+
"""Persist raw usage dict to disk (no lock — caller must hold lock)."""
|
|
297
|
+
self._usage_dir.mkdir(parents=True, exist_ok=True)
|
|
298
|
+
path = self._usage_dir / f"tokens-{date_str}.json"
|
|
299
|
+
try:
|
|
300
|
+
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
301
|
+
except OSError as exc:
|
|
302
|
+
logger.error("Failed to write usage file %s: %s", path, exc)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
# Helpers
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _today_str() -> str:
|
|
311
|
+
"""Return today's date in UTC as 'YYYY-MM-DD'."""
|
|
312
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _parse_date(date_str: str) -> date:
|
|
316
|
+
"""Parse 'YYYY-MM-DD' to a date object."""
|
|
317
|
+
return datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _raw_to_report(date_str: str, data: dict) -> DailyUsageReport:
|
|
321
|
+
"""Convert a raw usage dict to a DailyUsageReport."""
|
|
322
|
+
models: dict[str, ModelUsageSummary] = {}
|
|
323
|
+
for model, entry in data.get("models", {}).items():
|
|
324
|
+
models[model] = ModelUsageSummary(
|
|
325
|
+
model=model,
|
|
326
|
+
calls=entry.get("calls", 0),
|
|
327
|
+
input_tokens=entry.get("input_tokens", 0),
|
|
328
|
+
output_tokens=entry.get("output_tokens", 0),
|
|
329
|
+
estimated_cost_usd=entry.get("estimated_cost_usd", 0.0),
|
|
330
|
+
)
|
|
331
|
+
return DailyUsageReport(date=date_str, models=models)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ecosystem version checker for the sovereign agent stack.
|
|
3
|
+
|
|
4
|
+
Compares installed package versions against the latest available on PyPI.
|
|
5
|
+
Surfaces outdated packages in ``skcapstone doctor`` and provides a
|
|
6
|
+
standalone ``skcapstone version-check`` CLI command.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import urllib.request
|
|
13
|
+
import urllib.error
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ECOSYSTEM_PACKAGES = [
|
|
19
|
+
"skmemory",
|
|
20
|
+
"skcapstone",
|
|
21
|
+
"capauth",
|
|
22
|
+
"sksecurity",
|
|
23
|
+
"skcomm",
|
|
24
|
+
"skchat",
|
|
25
|
+
"cloud9-protocol",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class PackageVersion:
|
|
31
|
+
"""Version info for a single package.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
name: Package name.
|
|
35
|
+
installed: Installed version, or None if not installed.
|
|
36
|
+
latest: Latest version on PyPI, or None if unavailable.
|
|
37
|
+
up_to_date: Whether installed matches latest.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
name: str
|
|
41
|
+
installed: Optional[str] = None
|
|
42
|
+
latest: Optional[str] = None
|
|
43
|
+
up_to_date: bool = True
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class VersionReport:
|
|
48
|
+
"""Aggregated version report for the ecosystem.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
packages: List of per-package version info.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
packages: list[PackageVersion] = field(default_factory=list)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def all_up_to_date(self) -> bool:
|
|
58
|
+
"""Whether every installed package is up to date."""
|
|
59
|
+
return all(p.up_to_date for p in self.packages if p.installed)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def outdated(self) -> list[PackageVersion]:
|
|
63
|
+
"""Packages that are installed but not at the latest version."""
|
|
64
|
+
return [p for p in self.packages if p.installed and not p.up_to_date]
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def missing(self) -> list[PackageVersion]:
|
|
68
|
+
"""Packages that are not installed at all."""
|
|
69
|
+
return [p for p in self.packages if not p.installed]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _get_installed_version(package_name: str) -> Optional[str]:
|
|
73
|
+
"""Get the installed version of a package.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
package_name: Python package name.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Version string or None.
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
from importlib.metadata import version
|
|
83
|
+
|
|
84
|
+
return version(package_name)
|
|
85
|
+
except Exception:
|
|
86
|
+
# Try import-based fallback for packages with dashes
|
|
87
|
+
try:
|
|
88
|
+
mod_name = package_name.replace("-", "_")
|
|
89
|
+
import importlib
|
|
90
|
+
|
|
91
|
+
mod = importlib.import_module(mod_name)
|
|
92
|
+
return getattr(mod, "__version__", None)
|
|
93
|
+
except Exception:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _get_pypi_version(package_name: str, timeout: float = 5.0) -> Optional[str]:
|
|
98
|
+
"""Query PyPI JSON API for the latest version.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
package_name: Package name on PyPI.
|
|
102
|
+
timeout: HTTP timeout in seconds.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Latest version string, or None if unavailable.
|
|
106
|
+
"""
|
|
107
|
+
url = f"https://pypi.org/pypi/{package_name}/json"
|
|
108
|
+
try:
|
|
109
|
+
req = urllib.request.Request(url, headers={"Accept": "application/json"})
|
|
110
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
111
|
+
data = json.loads(resp.read().decode())
|
|
112
|
+
return data.get("info", {}).get("version")
|
|
113
|
+
except (urllib.error.URLError, json.JSONDecodeError, OSError):
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def check_versions(
|
|
118
|
+
packages: Optional[list[str]] = None,
|
|
119
|
+
check_pypi: bool = True,
|
|
120
|
+
) -> VersionReport:
|
|
121
|
+
"""Check installed vs latest versions for ecosystem packages.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
packages: Package names to check (default: ECOSYSTEM_PACKAGES).
|
|
125
|
+
check_pypi: Whether to query PyPI for latest versions.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
VersionReport with per-package results.
|
|
129
|
+
"""
|
|
130
|
+
pkg_list = packages or ECOSYSTEM_PACKAGES
|
|
131
|
+
report = VersionReport()
|
|
132
|
+
|
|
133
|
+
for name in pkg_list:
|
|
134
|
+
installed = _get_installed_version(name)
|
|
135
|
+
latest = _get_pypi_version(name) if check_pypi else None
|
|
136
|
+
|
|
137
|
+
up_to_date = True
|
|
138
|
+
if installed and latest:
|
|
139
|
+
up_to_date = installed == latest
|
|
140
|
+
|
|
141
|
+
report.packages.append(PackageVersion(
|
|
142
|
+
name=name,
|
|
143
|
+
installed=installed,
|
|
144
|
+
latest=latest,
|
|
145
|
+
up_to_date=up_to_date,
|
|
146
|
+
))
|
|
147
|
+
|
|
148
|
+
return report
|