@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,621 @@
|
|
|
1
|
+
"""SKSeed document ingestion CLI commands.
|
|
2
|
+
|
|
3
|
+
Turns documents (PDF, Markdown, TXT, HTML, URLs) into long-term memories
|
|
4
|
+
via the SKMemory store. Also validates seed.json files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from html.parser import HTMLParser
|
|
12
|
+
from io import StringIO
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
from urllib.parse import urlparse
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
from ._common import AGENT_HOME, console
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Text extraction helpers
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
class _HTMLStripper(HTMLParser):
|
|
27
|
+
"""Minimal HTML tag stripper using stdlib html.parser."""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
super().__init__()
|
|
31
|
+
self._parts: list[str] = []
|
|
32
|
+
|
|
33
|
+
def handle_data(self, data: str) -> None:
|
|
34
|
+
self._parts.append(data)
|
|
35
|
+
|
|
36
|
+
def get_text(self) -> str:
|
|
37
|
+
return "".join(self._parts)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _strip_html(html: str) -> str:
|
|
41
|
+
"""Remove HTML tags and return plain text."""
|
|
42
|
+
stripper = _HTMLStripper()
|
|
43
|
+
stripper.feed(html)
|
|
44
|
+
return stripper.get_text()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _extract_title_from_content(content: str, filename: str) -> str:
|
|
48
|
+
"""Extract a title from content (first heading) or fall back to filename."""
|
|
49
|
+
# Try markdown heading
|
|
50
|
+
for line in content.splitlines()[:20]:
|
|
51
|
+
stripped = line.strip()
|
|
52
|
+
if stripped.startswith("#"):
|
|
53
|
+
return stripped.lstrip("#").strip()
|
|
54
|
+
# Fall back to filename without extension
|
|
55
|
+
return Path(filename).stem
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _extract_key_claims(content: str, max_claims: int = 5) -> list[str]:
|
|
59
|
+
"""Extract key claims / sentences from content.
|
|
60
|
+
|
|
61
|
+
Simple heuristic: pick the first N non-trivial sentences.
|
|
62
|
+
"""
|
|
63
|
+
sentences = re.split(r"(?<=[.!?])\s+", content.strip())
|
|
64
|
+
claims: list[str] = []
|
|
65
|
+
for s in sentences:
|
|
66
|
+
s = s.strip()
|
|
67
|
+
if len(s) > 30 and not s.startswith("#"):
|
|
68
|
+
claims.append(s)
|
|
69
|
+
if len(claims) >= max_claims:
|
|
70
|
+
break
|
|
71
|
+
return claims
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _read_pdf(path: Path) -> str:
|
|
75
|
+
"""Read text from a PDF file. Uses PyMuPDF if available, else raw read."""
|
|
76
|
+
try:
|
|
77
|
+
import fitz # PyMuPDF
|
|
78
|
+
|
|
79
|
+
doc = fitz.open(str(path))
|
|
80
|
+
pages = []
|
|
81
|
+
for page in doc:
|
|
82
|
+
pages.append(page.get_text())
|
|
83
|
+
doc.close()
|
|
84
|
+
return "\n".join(pages)
|
|
85
|
+
except ImportError:
|
|
86
|
+
# Fallback: read raw bytes and try to extract printable text
|
|
87
|
+
raw = path.read_bytes()
|
|
88
|
+
text = raw.decode("utf-8", errors="ignore")
|
|
89
|
+
# Strip non-printable noise
|
|
90
|
+
return "".join(ch for ch in text if ch.isprintable() or ch in "\n\r\t")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _read_file(path: Path) -> tuple[str, str]:
|
|
94
|
+
"""Read a local file and return (content, content_type).
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Tuple of (extracted text, detected type string).
|
|
98
|
+
"""
|
|
99
|
+
suffix = path.suffix.lower()
|
|
100
|
+
|
|
101
|
+
if suffix == ".pdf":
|
|
102
|
+
return _read_pdf(path), "pdf"
|
|
103
|
+
elif suffix == ".html" or suffix == ".htm":
|
|
104
|
+
raw = path.read_text(encoding="utf-8", errors="replace")
|
|
105
|
+
return _strip_html(raw), "html"
|
|
106
|
+
elif suffix in (".md", ".markdown"):
|
|
107
|
+
return path.read_text(encoding="utf-8", errors="replace"), "markdown"
|
|
108
|
+
elif suffix == ".txt":
|
|
109
|
+
return path.read_text(encoding="utf-8", errors="replace"), "text"
|
|
110
|
+
elif suffix == ".json":
|
|
111
|
+
return path.read_text(encoding="utf-8", errors="replace"), "json"
|
|
112
|
+
else:
|
|
113
|
+
# Best-effort: read as text
|
|
114
|
+
return path.read_text(encoding="utf-8", errors="replace"), "text"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _fetch_url(url: str) -> tuple[str, str]:
|
|
118
|
+
"""Fetch a URL and return (content, content_type)."""
|
|
119
|
+
from urllib.request import urlopen, Request
|
|
120
|
+
|
|
121
|
+
req = Request(url, headers={"User-Agent": "skcapstone-skseed/1.0"})
|
|
122
|
+
with urlopen(req, timeout=30) as resp:
|
|
123
|
+
ctype = resp.headers.get("Content-Type", "text/plain")
|
|
124
|
+
raw = resp.read().decode("utf-8", errors="replace")
|
|
125
|
+
|
|
126
|
+
if "html" in ctype:
|
|
127
|
+
return _strip_html(raw), "html"
|
|
128
|
+
return raw, "text"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _get_memory_store():
|
|
132
|
+
"""Get a MemoryStore instance for the current agent."""
|
|
133
|
+
from skmemory.store import MemoryStore
|
|
134
|
+
|
|
135
|
+
return MemoryStore()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _generate_seed_json(
|
|
139
|
+
title: str,
|
|
140
|
+
content: str,
|
|
141
|
+
claims: list[str],
|
|
142
|
+
source_path: str,
|
|
143
|
+
content_type: str,
|
|
144
|
+
tags: Optional[list[str]] = None,
|
|
145
|
+
) -> dict:
|
|
146
|
+
"""Build a seed.json dict from extracted document content."""
|
|
147
|
+
from datetime import datetime, timezone
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
"seed_id": f"doc-{Path(source_path).stem}",
|
|
151
|
+
"version": "1.0",
|
|
152
|
+
"creator": {"model": "skseed-ingest", "instance": "cli"},
|
|
153
|
+
"experience": {
|
|
154
|
+
"summary": content[:2000],
|
|
155
|
+
"key_claims": claims,
|
|
156
|
+
},
|
|
157
|
+
"germination": {
|
|
158
|
+
"prompt": f"Document ingested from {source_path}: {title}",
|
|
159
|
+
},
|
|
160
|
+
"metadata": {
|
|
161
|
+
"source_path": source_path,
|
|
162
|
+
"content_type": content_type,
|
|
163
|
+
"ingested_at": datetime.now(timezone.utc).isoformat(),
|
|
164
|
+
"tags": tags or [],
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def ingest_document(
|
|
170
|
+
source: str,
|
|
171
|
+
title: Optional[str] = None,
|
|
172
|
+
tags: Optional[list[str]] = None,
|
|
173
|
+
output_seed: Optional[str] = None,
|
|
174
|
+
) -> dict:
|
|
175
|
+
"""Core ingestion logic shared by CLI and MCP.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
source: File path or URL to ingest.
|
|
179
|
+
title: Optional title override.
|
|
180
|
+
tags: Optional tags for the memory.
|
|
181
|
+
output_seed: Optional path to write seed.json to.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Dict with memory_id, title, summary, and seed_path keys.
|
|
185
|
+
"""
|
|
186
|
+
# Determine if source is URL or file
|
|
187
|
+
parsed = urlparse(source)
|
|
188
|
+
is_url = parsed.scheme in ("http", "https")
|
|
189
|
+
|
|
190
|
+
if is_url:
|
|
191
|
+
content, content_type = _fetch_url(source)
|
|
192
|
+
filename = parsed.path.split("/")[-1] or "web-document"
|
|
193
|
+
else:
|
|
194
|
+
path = Path(source).expanduser().resolve()
|
|
195
|
+
if not path.exists():
|
|
196
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
197
|
+
content, content_type = _read_file(path)
|
|
198
|
+
filename = path.name
|
|
199
|
+
|
|
200
|
+
if not content.strip():
|
|
201
|
+
raise ValueError("No content could be extracted from the source")
|
|
202
|
+
|
|
203
|
+
# Determine title
|
|
204
|
+
doc_title = title or _extract_title_from_content(content, filename)
|
|
205
|
+
|
|
206
|
+
# Extract key claims
|
|
207
|
+
claims = _extract_key_claims(content)
|
|
208
|
+
|
|
209
|
+
# Build seed JSON
|
|
210
|
+
seed_data = _generate_seed_json(
|
|
211
|
+
title=doc_title,
|
|
212
|
+
content=content,
|
|
213
|
+
claims=claims,
|
|
214
|
+
source_path=source,
|
|
215
|
+
content_type=content_type,
|
|
216
|
+
tags=tags,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Write seed.json if requested
|
|
220
|
+
seed_path = output_seed
|
|
221
|
+
if seed_path:
|
|
222
|
+
out = Path(seed_path)
|
|
223
|
+
out.write_text(json.dumps(seed_data, indent=2), encoding="utf-8")
|
|
224
|
+
|
|
225
|
+
# Store as long-term memory
|
|
226
|
+
all_tags = ["seed", "document", f"type:{content_type}"]
|
|
227
|
+
if tags:
|
|
228
|
+
all_tags.extend(tags)
|
|
229
|
+
|
|
230
|
+
store = _get_memory_store()
|
|
231
|
+
memory = store.snapshot(
|
|
232
|
+
title=f"Document: {doc_title}",
|
|
233
|
+
content=content[:10000], # Cap content for storage
|
|
234
|
+
layer="long-term",
|
|
235
|
+
source="skseed-ingest",
|
|
236
|
+
source_ref=source,
|
|
237
|
+
tags=all_tags,
|
|
238
|
+
metadata={
|
|
239
|
+
"key_claims": claims,
|
|
240
|
+
"content_type": content_type,
|
|
241
|
+
"original_length": len(content),
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
"memory_id": memory.id,
|
|
247
|
+
"title": doc_title,
|
|
248
|
+
"summary": content[:200].strip(),
|
|
249
|
+
"seed_path": seed_path,
|
|
250
|
+
"claims_count": len(claims),
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _validate_timestamp(value: str, field_name: str, result: dict) -> None:
|
|
255
|
+
"""Check that a string is a valid ISO 8601 timestamp.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
value: The timestamp string to validate.
|
|
259
|
+
field_name: Dotted field path for error messages.
|
|
260
|
+
result: The result dict to append errors/warnings to.
|
|
261
|
+
"""
|
|
262
|
+
from datetime import datetime
|
|
263
|
+
|
|
264
|
+
if not isinstance(value, str) or not value.strip():
|
|
265
|
+
result["warnings"].append(f"{field_name} is empty or not a string")
|
|
266
|
+
return
|
|
267
|
+
try:
|
|
268
|
+
datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
269
|
+
except (ValueError, TypeError):
|
|
270
|
+
result["errors"].append(
|
|
271
|
+
f"{field_name} is not a valid ISO 8601 timestamp: {value!r}"
|
|
272
|
+
)
|
|
273
|
+
result["valid"] = False
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _validate_tags(tags, field_name: str, result: dict) -> None:
|
|
277
|
+
"""Validate that tags is a list of non-empty strings.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
tags: The value to validate.
|
|
281
|
+
field_name: Dotted field path for error messages.
|
|
282
|
+
result: The result dict to append errors/warnings to.
|
|
283
|
+
"""
|
|
284
|
+
if not isinstance(tags, list):
|
|
285
|
+
result["errors"].append(f"{field_name} must be a list, got {type(tags).__name__}")
|
|
286
|
+
result["valid"] = False
|
|
287
|
+
return
|
|
288
|
+
for i, tag in enumerate(tags):
|
|
289
|
+
if not isinstance(tag, str):
|
|
290
|
+
result["errors"].append(
|
|
291
|
+
f"{field_name}[{i}] must be a string, got {type(tag).__name__}"
|
|
292
|
+
)
|
|
293
|
+
result["valid"] = False
|
|
294
|
+
elif not tag.strip():
|
|
295
|
+
result["warnings"].append(f"{field_name}[{i}] is an empty string")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _validate_emotional_signature(emo: dict, prefix: str, result: dict) -> None:
|
|
299
|
+
"""Validate an emotional_signature / emotional_snapshot block.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
emo: The emotional data dict.
|
|
303
|
+
prefix: Dotted field prefix for error messages.
|
|
304
|
+
result: The result dict to append errors/warnings to.
|
|
305
|
+
"""
|
|
306
|
+
if not isinstance(emo, dict):
|
|
307
|
+
result["warnings"].append(f"{prefix} should be a dict")
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
intensity = emo.get("intensity")
|
|
311
|
+
if intensity is not None:
|
|
312
|
+
if not isinstance(intensity, (int, float)):
|
|
313
|
+
result["errors"].append(f"{prefix}.intensity must be a number")
|
|
314
|
+
result["valid"] = False
|
|
315
|
+
elif not (0.0 <= float(intensity) <= 10.0):
|
|
316
|
+
result["warnings"].append(
|
|
317
|
+
f"{prefix}.intensity={intensity} is outside the expected 0-10 range"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
valence = emo.get("valence")
|
|
321
|
+
if valence is not None:
|
|
322
|
+
if not isinstance(valence, (int, float)):
|
|
323
|
+
result["errors"].append(f"{prefix}.valence must be a number")
|
|
324
|
+
result["valid"] = False
|
|
325
|
+
elif not (-1.0 <= float(valence) <= 1.0):
|
|
326
|
+
result["warnings"].append(
|
|
327
|
+
f"{prefix}.valence={valence} is outside the expected -1 to 1 range"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
labels = emo.get("labels", emo.get("emotions"))
|
|
331
|
+
if labels is not None:
|
|
332
|
+
_validate_tags(labels, f"{prefix}.labels", result)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def validate_seed_data(data: dict) -> dict:
|
|
336
|
+
"""Validate parsed seed data (dict) against the SKSeed schema.
|
|
337
|
+
|
|
338
|
+
This is the core validation logic, usable without a file path.
|
|
339
|
+
Both the CLI ``skseed validate`` command and the memory-store
|
|
340
|
+
import pipeline call this function.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
data: Parsed JSON seed data.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Dict with valid (bool), errors (list), warnings (list),
|
|
347
|
+
and fields (list) keys.
|
|
348
|
+
"""
|
|
349
|
+
result = {"valid": True, "errors": [], "warnings": [], "fields": []}
|
|
350
|
+
|
|
351
|
+
if not isinstance(data, dict):
|
|
352
|
+
result["valid"] = False
|
|
353
|
+
result["errors"].append("Top-level value must be a JSON object")
|
|
354
|
+
return result
|
|
355
|
+
|
|
356
|
+
# ---------------------------------------------------------------
|
|
357
|
+
# Detect format: Cloud9 (seed_metadata) vs standard
|
|
358
|
+
# ---------------------------------------------------------------
|
|
359
|
+
is_cloud9 = "seed_metadata" in data
|
|
360
|
+
|
|
361
|
+
if is_cloud9:
|
|
362
|
+
result["fields"].append("seed_metadata (Cloud9 format)")
|
|
363
|
+
meta = data.get("seed_metadata", {})
|
|
364
|
+
|
|
365
|
+
# Required: seed_id (inside seed_metadata or top-level)
|
|
366
|
+
seed_id = meta.get("seed_id") or data.get("seed_id")
|
|
367
|
+
if seed_id:
|
|
368
|
+
result["fields"].append("seed_id")
|
|
369
|
+
else:
|
|
370
|
+
result["valid"] = False
|
|
371
|
+
result["errors"].append(
|
|
372
|
+
"Missing required field: seed_id "
|
|
373
|
+
"(checked seed_metadata.seed_id and top-level)"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Required: version
|
|
377
|
+
version = meta.get("version") or data.get("version")
|
|
378
|
+
if version:
|
|
379
|
+
result["fields"].append("version")
|
|
380
|
+
else:
|
|
381
|
+
result["valid"] = False
|
|
382
|
+
result["errors"].append("Missing required field: version")
|
|
383
|
+
|
|
384
|
+
# Timestamps
|
|
385
|
+
if "created_at" in meta:
|
|
386
|
+
_validate_timestamp(meta["created_at"], "seed_metadata.created_at", result)
|
|
387
|
+
|
|
388
|
+
identity = data.get("identity", {})
|
|
389
|
+
if isinstance(identity, dict) and "timestamp" in identity:
|
|
390
|
+
_validate_timestamp(identity["timestamp"], "identity.timestamp", result)
|
|
391
|
+
|
|
392
|
+
# Experience summary (Cloud9 keeps it in experience_summary.narrative)
|
|
393
|
+
exp = data.get("experience_summary", {})
|
|
394
|
+
if isinstance(exp, dict):
|
|
395
|
+
result["fields"].append("experience_summary")
|
|
396
|
+
narrative = exp.get("narrative", "")
|
|
397
|
+
if not narrative or not str(narrative).strip():
|
|
398
|
+
result["warnings"].append("experience_summary.narrative is empty")
|
|
399
|
+
emo = exp.get("emotional_snapshot") or exp.get("emotional_signature")
|
|
400
|
+
if emo:
|
|
401
|
+
_validate_emotional_signature(
|
|
402
|
+
emo, "experience_summary.emotional_snapshot", result,
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
result["warnings"].append(
|
|
406
|
+
"Missing recommended field: experience_summary"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# Germination prompt
|
|
410
|
+
gp = data.get("germination_prompt")
|
|
411
|
+
if gp:
|
|
412
|
+
result["fields"].append("germination_prompt")
|
|
413
|
+
if isinstance(gp, str) and not gp.strip():
|
|
414
|
+
result["warnings"].append("germination_prompt is empty")
|
|
415
|
+
else:
|
|
416
|
+
result["warnings"].append("Missing recommended field: germination_prompt")
|
|
417
|
+
|
|
418
|
+
else:
|
|
419
|
+
# ---- Standard format ----
|
|
420
|
+
# Required fields
|
|
421
|
+
for field in ("seed_id", "version"):
|
|
422
|
+
if field in data:
|
|
423
|
+
result["fields"].append(field)
|
|
424
|
+
if isinstance(data[field], str) and not data[field].strip():
|
|
425
|
+
result["errors"].append(f"Required field {field} is empty")
|
|
426
|
+
result["valid"] = False
|
|
427
|
+
else:
|
|
428
|
+
result["valid"] = False
|
|
429
|
+
result["errors"].append(f"Missing required field: {field}")
|
|
430
|
+
|
|
431
|
+
# Recommended fields
|
|
432
|
+
for field in ("creator", "experience", "germination"):
|
|
433
|
+
if field in data:
|
|
434
|
+
result["fields"].append(field)
|
|
435
|
+
else:
|
|
436
|
+
result["warnings"].append(f"Missing recommended field: {field}")
|
|
437
|
+
|
|
438
|
+
# Creator structure
|
|
439
|
+
creator = data.get("creator")
|
|
440
|
+
if isinstance(creator, dict):
|
|
441
|
+
if not creator.get("model") and not creator.get("instance"):
|
|
442
|
+
result["warnings"].append(
|
|
443
|
+
"creator should have at least 'model' or 'instance'"
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# Experience validation
|
|
447
|
+
exp = data.get("experience", {})
|
|
448
|
+
if isinstance(exp, dict):
|
|
449
|
+
summary = exp.get("summary", "")
|
|
450
|
+
if not summary or not str(summary).strip():
|
|
451
|
+
result["errors"].append("experience.summary is empty or missing")
|
|
452
|
+
result["valid"] = False
|
|
453
|
+
|
|
454
|
+
emo = exp.get("emotional_signature")
|
|
455
|
+
if emo:
|
|
456
|
+
_validate_emotional_signature(
|
|
457
|
+
emo, "experience.emotional_signature", result,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
key_claims = exp.get("key_claims")
|
|
461
|
+
if key_claims is not None:
|
|
462
|
+
_validate_tags(key_claims, "experience.key_claims", result)
|
|
463
|
+
|
|
464
|
+
# Germination
|
|
465
|
+
germ = data.get("germination", {})
|
|
466
|
+
if isinstance(germ, dict):
|
|
467
|
+
prompt = germ.get("prompt", "")
|
|
468
|
+
if not prompt or not str(prompt).strip():
|
|
469
|
+
result["warnings"].append("germination.prompt is empty")
|
|
470
|
+
|
|
471
|
+
# ---------------------------------------------------------------
|
|
472
|
+
# Common validations (both formats)
|
|
473
|
+
# ---------------------------------------------------------------
|
|
474
|
+
|
|
475
|
+
# metadata.tags
|
|
476
|
+
metadata = data.get("metadata", {})
|
|
477
|
+
if isinstance(metadata, dict):
|
|
478
|
+
tags = metadata.get("tags")
|
|
479
|
+
if tags is not None:
|
|
480
|
+
_validate_tags(tags, "metadata.tags", result)
|
|
481
|
+
ingested_at = metadata.get("ingested_at")
|
|
482
|
+
if ingested_at:
|
|
483
|
+
_validate_timestamp(ingested_at, "metadata.ingested_at", result)
|
|
484
|
+
|
|
485
|
+
# lineage
|
|
486
|
+
lineage = data.get("lineage")
|
|
487
|
+
if lineage is not None:
|
|
488
|
+
if not isinstance(lineage, list):
|
|
489
|
+
result["errors"].append("lineage must be a list")
|
|
490
|
+
result["valid"] = False
|
|
491
|
+
else:
|
|
492
|
+
for i, entry in enumerate(lineage):
|
|
493
|
+
if not isinstance(entry, (str, dict)):
|
|
494
|
+
result["errors"].append(
|
|
495
|
+
f"lineage[{i}] must be a string or object, "
|
|
496
|
+
f"got {type(entry).__name__}"
|
|
497
|
+
)
|
|
498
|
+
result["valid"] = False
|
|
499
|
+
|
|
500
|
+
# integrity checksum format
|
|
501
|
+
integrity = data.get("integrity", {})
|
|
502
|
+
if isinstance(integrity, dict):
|
|
503
|
+
checksum = integrity.get("checksum", "")
|
|
504
|
+
if checksum and ":" not in checksum:
|
|
505
|
+
result["warnings"].append(
|
|
506
|
+
"integrity.checksum should use 'algorithm:hex' format "
|
|
507
|
+
f"(e.g. sha256:abc...), got: {checksum!r}"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
return result
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def validate_seed_file(path: str) -> dict:
|
|
514
|
+
"""Validate a seed.json file.
|
|
515
|
+
|
|
516
|
+
Reads the file, parses JSON, and delegates to ``validate_seed_data``
|
|
517
|
+
for schema-level checks.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
path: Path to the seed.json file.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
Dict with valid (bool), errors (list), warnings (list),
|
|
524
|
+
and fields (list) keys.
|
|
525
|
+
"""
|
|
526
|
+
result = {"valid": True, "errors": [], "warnings": [], "fields": []}
|
|
527
|
+
file_path = Path(path).expanduser().resolve()
|
|
528
|
+
|
|
529
|
+
if not file_path.exists():
|
|
530
|
+
result["valid"] = False
|
|
531
|
+
result["errors"].append(f"File not found: {file_path}")
|
|
532
|
+
return result
|
|
533
|
+
|
|
534
|
+
# Check JSON validity
|
|
535
|
+
try:
|
|
536
|
+
raw = file_path.read_text(encoding="utf-8")
|
|
537
|
+
data = json.loads(raw)
|
|
538
|
+
except json.JSONDecodeError as e:
|
|
539
|
+
result["valid"] = False
|
|
540
|
+
result["errors"].append(f"Invalid JSON: {e}")
|
|
541
|
+
return result
|
|
542
|
+
|
|
543
|
+
return validate_seed_data(data)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
# ---------------------------------------------------------------------------
|
|
547
|
+
# CLI command group
|
|
548
|
+
# ---------------------------------------------------------------------------
|
|
549
|
+
|
|
550
|
+
def register_skseed_commands(main: click.Group) -> None:
|
|
551
|
+
"""Register the skseed command group."""
|
|
552
|
+
|
|
553
|
+
@main.group()
|
|
554
|
+
def skseed():
|
|
555
|
+
"""SKSeed — document ingestion and seed management.
|
|
556
|
+
|
|
557
|
+
Turn documents into memories. Validate seed files.
|
|
558
|
+
"""
|
|
559
|
+
|
|
560
|
+
@skseed.command("ingest")
|
|
561
|
+
@click.argument("source")
|
|
562
|
+
@click.option("--title", "-t", default=None, help="Override document title.")
|
|
563
|
+
@click.option(
|
|
564
|
+
"--tag", "-g", multiple=True, help="Tags to attach to the memory."
|
|
565
|
+
)
|
|
566
|
+
@click.option(
|
|
567
|
+
"--output-seed", "-o", default=None,
|
|
568
|
+
help="Path to write seed.json output.",
|
|
569
|
+
)
|
|
570
|
+
def ingest_cmd(source, title, tag, output_seed):
|
|
571
|
+
"""Ingest a document (file or URL) into memory.
|
|
572
|
+
|
|
573
|
+
Supports: .pdf, .md, .txt, .html files and http(s) URLs.
|
|
574
|
+
Extracts text, identifies key claims, stores as long-term memory.
|
|
575
|
+
"""
|
|
576
|
+
try:
|
|
577
|
+
result = ingest_document(
|
|
578
|
+
source=source,
|
|
579
|
+
title=title,
|
|
580
|
+
tags=list(tag) if tag else None,
|
|
581
|
+
output_seed=output_seed,
|
|
582
|
+
)
|
|
583
|
+
console.print(f"Ingested: {result['title']} -> {result['memory_id']}")
|
|
584
|
+
if result.get("seed_path"):
|
|
585
|
+
console.print(f" Seed written to: {result['seed_path']}")
|
|
586
|
+
console.print(f" Claims extracted: {result['claims_count']}")
|
|
587
|
+
except FileNotFoundError as e:
|
|
588
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
589
|
+
raise SystemExit(1)
|
|
590
|
+
except ValueError as e:
|
|
591
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
592
|
+
raise SystemExit(1)
|
|
593
|
+
except Exception as e:
|
|
594
|
+
console.print(f"[red]Ingestion failed:[/red] {e}")
|
|
595
|
+
raise SystemExit(1)
|
|
596
|
+
|
|
597
|
+
@skseed.command("validate")
|
|
598
|
+
@click.argument("file")
|
|
599
|
+
def validate_cmd(file):
|
|
600
|
+
"""Validate a seed.json file.
|
|
601
|
+
|
|
602
|
+
Checks JSON validity, required fields, and recommended structure.
|
|
603
|
+
"""
|
|
604
|
+
result = validate_seed_file(file)
|
|
605
|
+
|
|
606
|
+
if result["valid"]:
|
|
607
|
+
console.print("[green]VALID[/green] seed file")
|
|
608
|
+
else:
|
|
609
|
+
console.print("[red]INVALID[/red] seed file")
|
|
610
|
+
|
|
611
|
+
if result["fields"]:
|
|
612
|
+
console.print(f" Fields found: {', '.join(result['fields'])}")
|
|
613
|
+
|
|
614
|
+
for err in result["errors"]:
|
|
615
|
+
console.print(f" [red]ERROR:[/red] {err}")
|
|
616
|
+
|
|
617
|
+
for warn in result["warnings"]:
|
|
618
|
+
console.print(f" [yellow]WARNING:[/yellow] {warn}")
|
|
619
|
+
|
|
620
|
+
if not result["valid"]:
|
|
621
|
+
raise SystemExit(1)
|