@heytherevibin/skillforge 0.2.1
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/CHANGELOG.md +16 -0
- package/CODE_OF_CONDUCT.md +34 -0
- package/CONTRIBUTING.md +38 -0
- package/LICENSE +21 -0
- package/README.md +337 -0
- package/RELEASING.md +93 -0
- package/SECURITY.md +31 -0
- package/STRATEGY.md +26 -0
- package/bin/cli.js +547 -0
- package/lib/packs.js +184 -0
- package/package.json +38 -0
- package/python/app/__init__.py +0 -0
- package/python/app/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/app/__pycache__/auth.cpython-312.pyc +0 -0
- package/python/app/__pycache__/main.cpython-312.pyc +0 -0
- package/python/app/auth.py +63 -0
- package/python/app/cli.py +78 -0
- package/python/app/db_paths.py +26 -0
- package/python/app/events_cli.py +175 -0
- package/python/app/main.py +647 -0
- package/python/app/materialize.py +138 -0
- package/python/app/mcp_server.py +610 -0
- package/python/app/route_cli.py +117 -0
- package/python/requirements-dev.txt +1 -0
- package/python/requirements.txt +7 -0
- package/python/tests/test_db_paths.py +41 -0
- package/skills/accessibility/SKILL.md +145 -0
- package/skills/agent-architecture-audit/SKILL.md +256 -0
- package/skills/agent-eval/SKILL.md +144 -0
- package/skills/agent-harness-construction/SKILL.md +72 -0
- package/skills/agent-introspection-debugging/SKILL.md +152 -0
- package/skills/agent-payment-x402/SKILL.md +224 -0
- package/skills/agent-sort/SKILL.md +214 -0
- package/skills/agentic-engineering/SKILL.md +62 -0
- package/skills/agentic-os/SKILL.md +386 -0
- package/skills/ai-first-engineering/SKILL.md +50 -0
- package/skills/ai-regression-testing/SKILL.md +384 -0
- package/skills/android-clean-architecture/SKILL.md +338 -0
- package/skills/angular-developer/SKILL.md +153 -0
- package/skills/angular-developer/references/angular-animations.md +160 -0
- package/skills/angular-developer/references/angular-aria.md +410 -0
- package/skills/angular-developer/references/cli.md +86 -0
- package/skills/angular-developer/references/component-harnesses.md +59 -0
- package/skills/angular-developer/references/component-styling.md +91 -0
- package/skills/angular-developer/references/components.md +117 -0
- package/skills/angular-developer/references/creating-services.md +97 -0
- package/skills/angular-developer/references/data-resolvers.md +69 -0
- package/skills/angular-developer/references/define-routes.md +67 -0
- package/skills/angular-developer/references/defining-providers.md +72 -0
- package/skills/angular-developer/references/di-fundamentals.md +120 -0
- package/skills/angular-developer/references/e2e-testing.md +56 -0
- package/skills/angular-developer/references/effects.md +83 -0
- package/skills/angular-developer/references/hierarchical-injectors.md +43 -0
- package/skills/angular-developer/references/host-elements.md +80 -0
- package/skills/angular-developer/references/injection-context.md +63 -0
- package/skills/angular-developer/references/inputs.md +101 -0
- package/skills/angular-developer/references/linked-signal.md +59 -0
- package/skills/angular-developer/references/loading-strategies.md +61 -0
- package/skills/angular-developer/references/mcp.md +108 -0
- package/skills/angular-developer/references/navigate-to-routes.md +69 -0
- package/skills/angular-developer/references/outputs.md +86 -0
- package/skills/angular-developer/references/reactive-forms.md +122 -0
- package/skills/angular-developer/references/rendering-strategies.md +44 -0
- package/skills/angular-developer/references/resource.md +77 -0
- package/skills/angular-developer/references/route-animations.md +56 -0
- package/skills/angular-developer/references/route-guards.md +52 -0
- package/skills/angular-developer/references/router-lifecycle.md +45 -0
- package/skills/angular-developer/references/router-testing.md +87 -0
- package/skills/angular-developer/references/show-routes-with-outlets.md +68 -0
- package/skills/angular-developer/references/signal-forms.md +795 -0
- package/skills/angular-developer/references/signals-overview.md +94 -0
- package/skills/angular-developer/references/tailwind-css.md +69 -0
- package/skills/angular-developer/references/template-driven-forms.md +114 -0
- package/skills/angular-developer/references/testing-fundamentals.md +65 -0
- package/skills/api-connector-builder/SKILL.md +120 -0
- package/skills/api-design/SKILL.md +522 -0
- package/skills/architecture-decision-records/SKILL.md +178 -0
- package/skills/article-writing/SKILL.md +78 -0
- package/skills/automation-audit-ops/SKILL.md +141 -0
- package/skills/autonomous-agent-harness/SKILL.md +272 -0
- package/skills/autonomous-loops/SKILL.md +609 -0
- package/skills/backend-patterns/SKILL.md +560 -0
- package/skills/benchmark/SKILL.md +92 -0
- package/skills/blueprint/SKILL.md +104 -0
- package/skills/browser-qa/SKILL.md +86 -0
- package/skills/bun-runtime/SKILL.md +83 -0
- package/skills/canary-watch/SKILL.md +98 -0
- package/skills/carrier-relationship-management/SKILL.md +211 -0
- package/skills/cisco-ios-patterns/SKILL.md +163 -0
- package/skills/ck/SKILL.md +147 -0
- package/skills/ck/commands/forget.mjs +44 -0
- package/skills/ck/commands/info.mjs +24 -0
- package/skills/ck/commands/init.mjs +143 -0
- package/skills/ck/commands/list.mjs +40 -0
- package/skills/ck/commands/migrate.mjs +202 -0
- package/skills/ck/commands/resume.mjs +36 -0
- package/skills/ck/commands/save.mjs +210 -0
- package/skills/ck/commands/shared.mjs +387 -0
- package/skills/ck/hooks/session-start.mjs +224 -0
- package/skills/claude-devfleet/SKILL.md +103 -0
- package/skills/click-path-audit/SKILL.md +244 -0
- package/skills/clickhouse-io/SKILL.md +438 -0
- package/skills/code-tour/SKILL.md +235 -0
- package/skills/codebase-onboarding/SKILL.md +232 -0
- package/skills/coding-standards/SKILL.md +548 -0
- package/skills/compose-multiplatform-patterns/SKILL.md +298 -0
- package/skills/connections-optimizer/SKILL.md +188 -0
- package/skills/content-engine/SKILL.md +126 -0
- package/skills/content-hash-cache-pattern/SKILL.md +160 -0
- package/skills/context-budget/SKILL.md +134 -0
- package/skills/continuous-agent-loop/SKILL.md +44 -0
- package/skills/continuous-learning/SKILL.md +129 -0
- package/skills/continuous-learning/config.json +18 -0
- package/skills/continuous-learning/evaluate-session.sh +69 -0
- package/skills/continuous-learning-v2/SKILL.md +358 -0
- package/skills/continuous-learning-v2/agents/observer-loop.sh +322 -0
- package/skills/continuous-learning-v2/agents/observer.md +198 -0
- package/skills/continuous-learning-v2/agents/session-guardian.sh +150 -0
- package/skills/continuous-learning-v2/agents/start-observer.sh +248 -0
- package/skills/continuous-learning-v2/config.json +8 -0
- package/skills/continuous-learning-v2/hooks/observe.sh +476 -0
- package/skills/continuous-learning-v2/scripts/detect-project.sh +288 -0
- package/skills/continuous-learning-v2/scripts/instinct-cli.py +1519 -0
- package/skills/continuous-learning-v2/scripts/lib/homunculus-dir.sh +31 -0
- package/skills/continuous-learning-v2/scripts/migrate-homunculus.sh +62 -0
- package/skills/continuous-learning-v2/scripts/test_parse_instinct.py +1018 -0
- package/skills/cost-aware-llm-pipeline/SKILL.md +182 -0
- package/skills/cost-tracking/SKILL.md +147 -0
- package/skills/council/SKILL.md +202 -0
- package/skills/cpp-coding-standards/SKILL.md +722 -0
- package/skills/cpp-testing/SKILL.md +323 -0
- package/skills/crosspost/SKILL.md +110 -0
- package/skills/csharp-testing/SKILL.md +320 -0
- package/skills/customer-billing-ops/SKILL.md +139 -0
- package/skills/customs-trade-compliance/SKILL.md +262 -0
- package/skills/dart-flutter-patterns/SKILL.md +562 -0
- package/skills/dashboard-builder/SKILL.md +108 -0
- package/skills/data-scraper-agent/SKILL.md +764 -0
- package/skills/database-migrations/SKILL.md +428 -0
- package/skills/deep-research/SKILL.md +158 -0
- package/skills/defi-amm-security/SKILL.md +166 -0
- package/skills/deployment-patterns/SKILL.md +426 -0
- package/skills/design-system/SKILL.md +81 -0
- package/skills/django-celery/SKILL.md +456 -0
- package/skills/django-patterns/SKILL.md +733 -0
- package/skills/django-security/SKILL.md +592 -0
- package/skills/django-tdd/SKILL.md +728 -0
- package/skills/django-verification/SKILL.md +468 -0
- package/skills/dmux-workflows/SKILL.md +190 -0
- package/skills/docker-patterns/SKILL.md +363 -0
- package/skills/documentation-lookup/SKILL.md +89 -0
- package/skills/dotnet-patterns/SKILL.md +320 -0
- package/skills/e2e-testing/SKILL.md +325 -0
- package/skills/email-ops/SKILL.md +120 -0
- package/skills/energy-procurement/SKILL.md +227 -0
- package/skills/enterprise-agent-ops/SKILL.md +49 -0
- package/skills/error-handling/SKILL.md +375 -0
- package/skills/eval-harness/SKILL.md +269 -0
- package/skills/evm-token-decimals/SKILL.md +130 -0
- package/skills/exa-search/SKILL.md +106 -0
- package/skills/fal-ai-media/SKILL.md +287 -0
- package/skills/fastapi-patterns/SKILL.md +327 -0
- package/skills/finance-billing-ops/SKILL.md +126 -0
- package/skills/flox-environments/SKILL.md +496 -0
- package/skills/flutter-dart-code-review/SKILL.md +434 -0
- package/skills/foundation-models-on-device/SKILL.md +243 -0
- package/skills/frontend-design-direction/SKILL.md +92 -0
- package/skills/frontend-patterns/SKILL.md +641 -0
- package/skills/frontend-slides/SKILL.md +183 -0
- package/skills/frontend-slides/STYLE_PRESETS.md +330 -0
- package/skills/frontend-slides/animation-patterns.md +122 -0
- package/skills/frontend-slides/html-template.md +419 -0
- package/skills/frontend-slides/scripts/export-pdf.sh +418 -0
- package/skills/frontend-slides/scripts/extract-pptx.py +96 -0
- package/skills/frontend-slides/viewport-base.css +153 -0
- package/skills/fsharp-testing/SKILL.md +279 -0
- package/skills/gan-style-harness/SKILL.md +278 -0
- package/skills/gateguard/SKILL.md +125 -0
- package/skills/git-workflow/SKILL.md +714 -0
- package/skills/github-ops/SKILL.md +143 -0
- package/skills/golang-patterns/SKILL.md +673 -0
- package/skills/golang-testing/SKILL.md +719 -0
- package/skills/google-workspace-ops/SKILL.md +94 -0
- package/skills/healthcare-cdss-patterns/SKILL.md +245 -0
- package/skills/healthcare-emr-patterns/SKILL.md +159 -0
- package/skills/healthcare-eval-harness/SKILL.md +207 -0
- package/skills/healthcare-phi-compliance/SKILL.md +145 -0
- package/skills/hermes-imports/SKILL.md +87 -0
- package/skills/hexagonal-architecture/SKILL.md +275 -0
- package/skills/hipaa-compliance/SKILL.md +78 -0
- package/skills/homelab-network-readiness/SKILL.md +169 -0
- package/skills/homelab-network-setup/SKILL.md +129 -0
- package/skills/homelab-pihole-dns/SKILL.md +274 -0
- package/skills/homelab-vlan-segmentation/SKILL.md +311 -0
- package/skills/homelab-wireguard-vpn/SKILL.md +305 -0
- package/skills/hookify-rules/SKILL.md +128 -0
- package/skills/inventory-demand-planning/SKILL.md +246 -0
- package/skills/investor-materials/SKILL.md +95 -0
- package/skills/investor-outreach/SKILL.md +90 -0
- package/skills/ios-icon-gen/SKILL.md +157 -0
- package/skills/ios-icon-gen/scripts/generate_icons.swift +258 -0
- package/skills/ios-icon-gen/scripts/iconify_gen.sh +235 -0
- package/skills/iterative-retrieval/SKILL.md +209 -0
- package/skills/java-coding-standards/SKILL.md +382 -0
- package/skills/jira-integration/SKILL.md +292 -0
- package/skills/jpa-patterns/SKILL.md +150 -0
- package/skills/knowledge-ops/SKILL.md +153 -0
- package/skills/kotlin-coroutines-flows/SKILL.md +283 -0
- package/skills/kotlin-exposed-patterns/SKILL.md +718 -0
- package/skills/kotlin-ktor-patterns/SKILL.md +688 -0
- package/skills/kotlin-patterns/SKILL.md +710 -0
- package/skills/kotlin-testing/SKILL.md +823 -0
- package/skills/laravel-patterns/SKILL.md +414 -0
- package/skills/laravel-plugin-discovery/SKILL.md +228 -0
- package/skills/laravel-security/SKILL.md +284 -0
- package/skills/laravel-tdd/SKILL.md +282 -0
- package/skills/laravel-verification/SKILL.md +178 -0
- package/skills/lead-intelligence/SKILL.md +320 -0
- package/skills/lead-intelligence/agents/enrichment-agent.md +85 -0
- package/skills/lead-intelligence/agents/mutual-mapper.md +75 -0
- package/skills/lead-intelligence/agents/outreach-drafter.md +98 -0
- package/skills/lead-intelligence/agents/signal-scorer.md +60 -0
- package/skills/liquid-glass-design/SKILL.md +279 -0
- package/skills/llm-trading-agent-security/SKILL.md +146 -0
- package/skills/logistics-exception-management/SKILL.md +221 -0
- package/skills/make-interfaces-feel-better/SKILL.md +151 -0
- package/skills/manim-video/SKILL.md +88 -0
- package/skills/manim-video/assets/network_graph_scene.py +52 -0
- package/skills/market-research/SKILL.md +74 -0
- package/skills/mcp-server-patterns/SKILL.md +68 -0
- package/skills/messages-ops/SKILL.md +103 -0
- package/skills/mle-workflow/SKILL.md +345 -0
- package/skills/motion-advanced/SKILL.md +596 -0
- package/skills/motion-foundations/SKILL.md +299 -0
- package/skills/motion-patterns/SKILL.md +435 -0
- package/skills/motion-ui/SKILL.md +574 -0
- package/skills/mysql-patterns/SKILL.md +411 -0
- package/skills/nanoclaw-repl/SKILL.md +32 -0
- package/skills/nestjs-patterns/SKILL.md +229 -0
- package/skills/netmiko-ssh-automation/SKILL.md +173 -0
- package/skills/network-bgp-diagnostics/SKILL.md +167 -0
- package/skills/network-config-validation/SKILL.md +210 -0
- package/skills/network-interface-health/SKILL.md +152 -0
- package/skills/nextjs-turbopack/SKILL.md +43 -0
- package/skills/nodejs-keccak256/SKILL.md +102 -0
- package/skills/nutrient-document-processing/SKILL.md +166 -0
- package/skills/nuxt4-patterns/SKILL.md +99 -0
- package/skills/openclaw-persona-forge/SKILL.md +288 -0
- package/skills/openclaw-persona-forge/gacha.py +224 -0
- package/skills/openclaw-persona-forge/gacha.sh +5 -0
- package/skills/openclaw-persona-forge/references/avatar-style.md +124 -0
- package/skills/openclaw-persona-forge/references/boundary-rules.md +53 -0
- package/skills/openclaw-persona-forge/references/error-handling.md +53 -0
- package/skills/openclaw-persona-forge/references/identity-tension.md +48 -0
- package/skills/openclaw-persona-forge/references/naming-system.md +39 -0
- package/skills/openclaw-persona-forge/references/output-template.md +166 -0
- package/skills/opensource-pipeline/SKILL.md +254 -0
- package/skills/perl-patterns/SKILL.md +503 -0
- package/skills/perl-security/SKILL.md +502 -0
- package/skills/perl-testing/SKILL.md +474 -0
- package/skills/plan-orchestrate/SKILL.md +253 -0
- package/skills/plankton-code-quality/SKILL.md +236 -0
- package/skills/postgres-patterns/SKILL.md +146 -0
- package/skills/product-capability/SKILL.md +140 -0
- package/skills/product-lens/SKILL.md +91 -0
- package/skills/production-audit/SKILL.md +206 -0
- package/skills/production-scheduling/SKILL.md +237 -0
- package/skills/project-flow-ops/SKILL.md +110 -0
- package/skills/prompt-optimizer/SKILL.md +398 -0
- package/skills/python-patterns/SKILL.md +749 -0
- package/skills/python-testing/SKILL.md +815 -0
- package/skills/pytorch-patterns/SKILL.md +395 -0
- package/skills/quality-nonconformance/SKILL.md +259 -0
- package/skills/quarkus-patterns/SKILL.md +721 -0
- package/skills/quarkus-security/SKILL.md +466 -0
- package/skills/quarkus-tdd/SKILL.md +810 -0
- package/skills/quarkus-verification/SKILL.md +478 -0
- package/skills/ralphinho-rfc-pipeline/SKILL.md +66 -0
- package/skills/redis-patterns/SKILL.md +402 -0
- package/skills/regex-vs-llm-structured-text/SKILL.md +219 -0
- package/skills/remotion-video-creation/SKILL.md +43 -0
- package/skills/remotion-video-creation/rules/3d.md +86 -0
- package/skills/remotion-video-creation/rules/animations.md +29 -0
- package/skills/remotion-video-creation/rules/assets/charts-bar-chart.tsx +173 -0
- package/skills/remotion-video-creation/rules/assets/text-animations-typewriter.tsx +100 -0
- package/skills/remotion-video-creation/rules/assets/text-animations-word-highlight.tsx +108 -0
- package/skills/remotion-video-creation/rules/assets.md +78 -0
- package/skills/remotion-video-creation/rules/audio.md +172 -0
- package/skills/remotion-video-creation/rules/calculate-metadata.md +104 -0
- package/skills/remotion-video-creation/rules/can-decode.md +75 -0
- package/skills/remotion-video-creation/rules/charts.md +58 -0
- package/skills/remotion-video-creation/rules/compositions.md +146 -0
- package/skills/remotion-video-creation/rules/display-captions.md +126 -0
- package/skills/remotion-video-creation/rules/extract-frames.md +229 -0
- package/skills/remotion-video-creation/rules/fonts.md +152 -0
- package/skills/remotion-video-creation/rules/get-audio-duration.md +58 -0
- package/skills/remotion-video-creation/rules/get-video-dimensions.md +68 -0
- package/skills/remotion-video-creation/rules/get-video-duration.md +58 -0
- package/skills/remotion-video-creation/rules/gifs.md +138 -0
- package/skills/remotion-video-creation/rules/images.md +130 -0
- package/skills/remotion-video-creation/rules/import-srt-captions.md +67 -0
- package/skills/remotion-video-creation/rules/lottie.md +67 -0
- package/skills/remotion-video-creation/rules/measuring-dom-nodes.md +34 -0
- package/skills/remotion-video-creation/rules/measuring-text.md +143 -0
- package/skills/remotion-video-creation/rules/sequencing.md +106 -0
- package/skills/remotion-video-creation/rules/tailwind.md +11 -0
- package/skills/remotion-video-creation/rules/text-animations.md +20 -0
- package/skills/remotion-video-creation/rules/timing.md +179 -0
- package/skills/remotion-video-creation/rules/transcribe-captions.md +19 -0
- package/skills/remotion-video-creation/rules/transitions.md +122 -0
- package/skills/remotion-video-creation/rules/trimming.md +52 -0
- package/skills/remotion-video-creation/rules/videos.md +171 -0
- package/skills/repo-scan/SKILL.md +78 -0
- package/skills/research-ops/SKILL.md +111 -0
- package/skills/returns-reverse-logistics/SKILL.md +239 -0
- package/skills/rules-distill/SKILL.md +263 -0
- package/skills/rules-distill/scripts/scan-rules.sh +58 -0
- package/skills/rules-distill/scripts/scan-skills.sh +129 -0
- package/skills/rust-patterns/SKILL.md +498 -0
- package/skills/rust-testing/SKILL.md +499 -0
- package/skills/safety-guard/SKILL.md +74 -0
- package/skills/santa-method/SKILL.md +306 -0
- package/skills/scientific-db-pubmed-database/SKILL.md +175 -0
- package/skills/scientific-db-uspto-database/SKILL.md +177 -0
- package/skills/scientific-pkg-gget/SKILL.md +166 -0
- package/skills/scientific-thinking-literature-review/SKILL.md +192 -0
- package/skills/scientific-thinking-scholar-evaluation/SKILL.md +160 -0
- package/skills/search-first/SKILL.md +181 -0
- package/skills/security-bounty-hunter/SKILL.md +99 -0
- package/skills/security-review/SKILL.md +502 -0
- package/skills/security-review/cloud-infrastructure-security.md +361 -0
- package/skills/seo/SKILL.md +153 -0
- package/skills/skill-comply/SKILL.md +57 -0
- package/skills/skill-comply/fixtures/compliant_trace.jsonl +5 -0
- package/skills/skill-comply/fixtures/noncompliant_trace.jsonl +3 -0
- package/skills/skill-comply/fixtures/tdd_spec.yaml +44 -0
- package/skills/skill-comply/prompts/classifier.md +24 -0
- package/skills/skill-comply/prompts/scenario_generator.md +62 -0
- package/skills/skill-comply/prompts/spec_generator.md +42 -0
- package/skills/skill-comply/pyproject.toml +15 -0
- package/skills/skill-comply/scripts/__init__.py +0 -0
- package/skills/skill-comply/scripts/classifier.py +85 -0
- package/skills/skill-comply/scripts/grader.py +124 -0
- package/skills/skill-comply/scripts/parser.py +107 -0
- package/skills/skill-comply/scripts/report.py +170 -0
- package/skills/skill-comply/scripts/run.py +127 -0
- package/skills/skill-comply/scripts/runner.py +186 -0
- package/skills/skill-comply/scripts/scenario_generator.py +70 -0
- package/skills/skill-comply/scripts/spec_generator.py +72 -0
- package/skills/skill-comply/scripts/utils.py +13 -0
- package/skills/skill-comply/tests/test_grader.py +197 -0
- package/skills/skill-comply/tests/test_parser.py +90 -0
- package/skills/skill-comply/tests/test_runner.py +172 -0
- package/skills/skill-scout/SKILL.md +139 -0
- package/skills/skill-stocktake/SKILL.md +193 -0
- package/skills/skill-stocktake/scripts/quick-diff.sh +87 -0
- package/skills/skill-stocktake/scripts/save-results.sh +56 -0
- package/skills/skill-stocktake/scripts/scan.sh +170 -0
- package/skills/social-graph-ranker/SKILL.md +153 -0
- package/skills/springboot-patterns/SKILL.md +313 -0
- package/skills/springboot-security/SKILL.md +271 -0
- package/skills/springboot-tdd/SKILL.md +157 -0
- package/skills/springboot-verification/SKILL.md +230 -0
- package/skills/strategic-compact/SKILL.md +129 -0
- package/skills/strategic-compact/suggest-compact.sh +54 -0
- package/skills/swift-actor-persistence/SKILL.md +142 -0
- package/skills/swift-concurrency-6-2/SKILL.md +216 -0
- package/skills/swift-protocol-di-testing/SKILL.md +189 -0
- package/skills/swiftui-patterns/SKILL.md +259 -0
- package/skills/tdd-workflow/SKILL.md +462 -0
- package/skills/team-builder/SKILL.md +166 -0
- package/skills/terminal-ops/SKILL.md +108 -0
- package/skills/tinystruct-patterns/SKILL.md +130 -0
- package/skills/tinystruct-patterns/references/architecture.md +77 -0
- package/skills/tinystruct-patterns/references/data-handling.md +35 -0
- package/skills/tinystruct-patterns/references/routing.md +57 -0
- package/skills/tinystruct-patterns/references/system-usage.md +74 -0
- package/skills/tinystruct-patterns/references/testing.md +59 -0
- package/skills/token-budget-advisor/SKILL.md +133 -0
- package/skills/ui-demo/SKILL.md +464 -0
- package/skills/ui-to-vue/SKILL.md +134 -0
- package/skills/unified-notifications-ops/SKILL.md +186 -0
- package/skills/verification-loop/SKILL.md +125 -0
- package/skills/video-editing/SKILL.md +309 -0
- package/skills/videodb/SKILL.md +373 -0
- package/skills/videodb/reference/api-reference.md +550 -0
- package/skills/videodb/reference/capture-reference.md +407 -0
- package/skills/videodb/reference/capture.md +101 -0
- package/skills/videodb/reference/editor.md +443 -0
- package/skills/videodb/reference/generative.md +331 -0
- package/skills/videodb/reference/rtstream-reference.md +564 -0
- package/skills/videodb/reference/rtstream.md +65 -0
- package/skills/videodb/reference/search.md +230 -0
- package/skills/videodb/reference/streaming.md +406 -0
- package/skills/videodb/reference/use-cases.md +118 -0
- package/skills/videodb/scripts/ws_listener.py +282 -0
- package/skills/visa-doc-translate/README.md +86 -0
- package/skills/visa-doc-translate/SKILL.md +117 -0
- package/skills/vite-patterns/SKILL.md +448 -0
- package/skills/windows-desktop-e2e/SKILL.md +787 -0
- package/skills/workspace-surface-audit/SKILL.md +124 -0
- package/skills/x-api/SKILL.md +233 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
"""
|
|
2
|
+
skillforge — skill orchestrator co-tool for Claude (MCP-first).
|
|
3
|
+
|
|
4
|
+
Primary surface: MCP stdio — route_skills and related tools for hosts
|
|
5
|
+
(Claude Desktop, Cursor, Claude Code).
|
|
6
|
+
|
|
7
|
+
Optional: headless HTTP API (POST /chat, /events, …) for integrations.
|
|
8
|
+
Live usage: `skillforge events --watch` (terminal).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sqlite3
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
import uuid
|
|
19
|
+
from contextlib import asynccontextmanager
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Optional
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
from anthropic import AsyncAnthropic
|
|
26
|
+
from fastapi import FastAPI, Request
|
|
27
|
+
from fastapi.responses import StreamingResponse
|
|
28
|
+
from pydantic import BaseModel
|
|
29
|
+
from sentence_transformers import SentenceTransformer
|
|
30
|
+
|
|
31
|
+
from app.db_paths import global_db_path, resolve_orchestrator_db
|
|
32
|
+
|
|
33
|
+
# ---------- Config (env-driven so the Node wrapper controls paths) ----------
|
|
34
|
+
BUNDLED_SKILLS = Path(os.getenv("SKILLFORGE_BUNDLED_SKILLS", "./skills"))
|
|
35
|
+
USER_SKILLS = Path(os.getenv("SKILLFORGE_USER_SKILLS", str(Path.home() / ".skillforge" / "skills")))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
DB_PATH = global_db_path()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
EMBED_MODEL = os.getenv("SKILLFORGE_EMBED_MODEL", "all-MiniLM-L6-v2")
|
|
42
|
+
ROUTER_MODEL = os.getenv("SKILLFORGE_ROUTER_MODEL", "claude-haiku-4-5-20251001")
|
|
43
|
+
ANSWER_MODEL = os.getenv("SKILLFORGE_ANSWER_MODEL", "claude-opus-4-7")
|
|
44
|
+
TOP_K_CANDIDATES = int(os.getenv("SKILLFORGE_TOP_K", "15"))
|
|
45
|
+
MAX_ACTIVE_SKILLS = int(os.getenv("SKILLFORGE_MAX_ACTIVE", "7"))
|
|
46
|
+
REROUTE_THRESHOLD = float(os.getenv("SKILLFORGE_REROUTE_THRESHOLD", "0.4"))
|
|
47
|
+
# "" | "full" | "embedding" — embedding skips Haiku and takes top skills from the shortlist only.
|
|
48
|
+
SKILLFORGE_ROUTER_MODE = os.getenv("SKILLFORGE_ROUTER_MODE", "").strip().lower()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def build_router_and_skills(
|
|
52
|
+
*,
|
|
53
|
+
log: bool = True,
|
|
54
|
+
log_prefix: str = "[skillforge]",
|
|
55
|
+
) -> tuple[Router, dict[str, Skill]]:
|
|
56
|
+
"""Load embedding model, skill catalog, and Router (shared by MCP and ``skillforge route`` CLI)."""
|
|
57
|
+
if log:
|
|
58
|
+
print(f"{log_prefix} Loading skills...", file=sys.stderr)
|
|
59
|
+
skills = load_all_skills()
|
|
60
|
+
embed_model = SentenceTransformer(os.getenv("SKILLFORGE_EMBED_MODEL", "all-MiniLM-L6-v2"))
|
|
61
|
+
key = os.getenv("ANTHROPIC_API_KEY", "").strip()
|
|
62
|
+
mode = SKILLFORGE_ROUTER_MODE
|
|
63
|
+
if mode == "embedding":
|
|
64
|
+
anthropic = None
|
|
65
|
+
router_note = "embedding-only (SKILLFORGE_ROUTER_MODE=embedding)"
|
|
66
|
+
elif mode == "full":
|
|
67
|
+
if key:
|
|
68
|
+
anthropic = AsyncAnthropic()
|
|
69
|
+
router_note = "full Haiku router (SKILLFORGE_ROUTER_MODE=full)"
|
|
70
|
+
else:
|
|
71
|
+
anthropic = None
|
|
72
|
+
router_note = (
|
|
73
|
+
"embedding-only (SKILLFORGE_ROUTER_MODE=full but no ANTHROPIC_API_KEY — "
|
|
74
|
+
"Haiku routing skipped)"
|
|
75
|
+
)
|
|
76
|
+
elif key:
|
|
77
|
+
anthropic = AsyncAnthropic()
|
|
78
|
+
router_note = "full Haiku router (default; ANTHROPIC_API_KEY set)"
|
|
79
|
+
else:
|
|
80
|
+
anthropic = None
|
|
81
|
+
router_note = (
|
|
82
|
+
"embedding-only (no ANTHROPIC_API_KEY — keyless. "
|
|
83
|
+
"Set ANTHROPIC_API_KEY for Haiku routing.)"
|
|
84
|
+
)
|
|
85
|
+
if log:
|
|
86
|
+
print(f"{log_prefix} {router_note}", file=sys.stderr)
|
|
87
|
+
print(
|
|
88
|
+
f"{log_prefix} Loaded {len(skills)} skills from bundled={BUNDLED_SKILLS} user={USER_SKILLS}",
|
|
89
|
+
file=sys.stderr,
|
|
90
|
+
)
|
|
91
|
+
router = Router(skills, embed_model, anthropic)
|
|
92
|
+
skmap = {s.name: s for s in skills}
|
|
93
|
+
return router, skmap
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------- Skill loading ----------
|
|
97
|
+
@dataclass
|
|
98
|
+
class Skill:
|
|
99
|
+
name: str
|
|
100
|
+
title: str
|
|
101
|
+
description: str
|
|
102
|
+
body: str
|
|
103
|
+
source: str # "bundled" | "user"
|
|
104
|
+
disabled: bool = False
|
|
105
|
+
embedding: np.ndarray | None = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parse_skill_md(path: Path, source: str) -> Skill | None:
|
|
109
|
+
"""Parse a SKILL.md: extract frontmatter name/description + body.
|
|
110
|
+
|
|
111
|
+
Handles YAML block scalars (>, |) where description spans multiple
|
|
112
|
+
indented lines after the key.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
text = path.read_text(encoding="utf-8")
|
|
116
|
+
except Exception:
|
|
117
|
+
return None
|
|
118
|
+
name = path.parent.name
|
|
119
|
+
title = name.replace("-", " ").title()
|
|
120
|
+
description = ""
|
|
121
|
+
body = text
|
|
122
|
+
if text.startswith("---"):
|
|
123
|
+
end = text.find("---", 3)
|
|
124
|
+
if end != -1:
|
|
125
|
+
fm = text[3:end]
|
|
126
|
+
body = text[end + 3:].strip()
|
|
127
|
+
lines = fm.splitlines()
|
|
128
|
+
i = 0
|
|
129
|
+
while i < len(lines):
|
|
130
|
+
line = lines[i]
|
|
131
|
+
if ":" in line and not line.startswith(" ") and not line.startswith("\t"):
|
|
132
|
+
k, _, v = line.partition(":")
|
|
133
|
+
k = k.strip().lower()
|
|
134
|
+
v = v.strip()
|
|
135
|
+
# YAML block scalar: collect subsequent indented lines
|
|
136
|
+
if v in (">", "|", ">-", "|-", ">+", "|+"):
|
|
137
|
+
collected = []
|
|
138
|
+
j = i + 1
|
|
139
|
+
while j < len(lines) and (lines[j].startswith(" ") or lines[j].startswith("\t") or lines[j].strip() == ""):
|
|
140
|
+
collected.append(lines[j].strip())
|
|
141
|
+
j += 1
|
|
142
|
+
v = " ".join(s for s in collected if s)
|
|
143
|
+
i = j - 1
|
|
144
|
+
else:
|
|
145
|
+
v = v.strip('"').strip("'")
|
|
146
|
+
if k == "name":
|
|
147
|
+
title = v
|
|
148
|
+
elif k == "description":
|
|
149
|
+
description = v
|
|
150
|
+
i += 1
|
|
151
|
+
if not description:
|
|
152
|
+
for chunk in body.split("\n\n"):
|
|
153
|
+
chunk = chunk.strip()
|
|
154
|
+
if chunk and not chunk.startswith("#"):
|
|
155
|
+
description = chunk[:500]
|
|
156
|
+
break
|
|
157
|
+
return Skill(name=name, title=title, description=description, body=body, source=source)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def load_all_skills() -> list[Skill]:
|
|
161
|
+
"""Load from bundled dir first, then user dir (user overrides bundled by name)."""
|
|
162
|
+
by_name: dict[str, Skill] = {}
|
|
163
|
+
for src_dir, label in [(BUNDLED_SKILLS, "bundled"), (USER_SKILLS, "user")]:
|
|
164
|
+
if not src_dir.exists():
|
|
165
|
+
continue
|
|
166
|
+
for skill_md in sorted(src_dir.glob("*/SKILL.md")):
|
|
167
|
+
s = parse_skill_md(skill_md, label)
|
|
168
|
+
if s:
|
|
169
|
+
by_name[s.name] = s # later sources override
|
|
170
|
+
return list(by_name.values())
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def iter_skill_md_paths() -> list[Path]:
|
|
174
|
+
"""All discovered SKILL.md paths in load order (bundled, then user overrides)."""
|
|
175
|
+
paths: list[Path] = []
|
|
176
|
+
for src_dir in (BUNDLED_SKILLS, USER_SKILLS):
|
|
177
|
+
if not src_dir.exists():
|
|
178
|
+
continue
|
|
179
|
+
for skill_md in sorted(src_dir.glob("*/SKILL.md")):
|
|
180
|
+
paths.append(skill_md)
|
|
181
|
+
return paths
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def skill_catalog_manifest() -> tuple[tuple[str, int], ...]:
|
|
185
|
+
"""Stable on-disk fingerprint for MCP hot-reload (resolved path + mtime_ns)."""
|
|
186
|
+
rows: list[tuple[str, int]] = []
|
|
187
|
+
for skill_md in iter_skill_md_paths():
|
|
188
|
+
try:
|
|
189
|
+
mtime_ns = int(skill_md.stat().st_mtime_ns)
|
|
190
|
+
except OSError:
|
|
191
|
+
mtime_ns = 0
|
|
192
|
+
rows.append((str(skill_md.resolve()), mtime_ns))
|
|
193
|
+
return tuple(rows)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------- Database ----------
|
|
197
|
+
def init_db(db_file: Path | None = None):
|
|
198
|
+
path = global_db_path() if db_file is None else db_file
|
|
199
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
200
|
+
con = sqlite3.connect(str(path))
|
|
201
|
+
con.executescript("""
|
|
202
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
203
|
+
id TEXT PRIMARY KEY,
|
|
204
|
+
ts REAL NOT NULL,
|
|
205
|
+
user_id TEXT DEFAULT '',
|
|
206
|
+
session_id TEXT,
|
|
207
|
+
event_type TEXT,
|
|
208
|
+
payload TEXT
|
|
209
|
+
);
|
|
210
|
+
CREATE TABLE IF NOT EXISTS skill_weights (
|
|
211
|
+
user_id TEXT DEFAULT '',
|
|
212
|
+
skill_name TEXT,
|
|
213
|
+
weight REAL DEFAULT 0.0,
|
|
214
|
+
uses INTEGER DEFAULT 0,
|
|
215
|
+
referenced INTEGER DEFAULT 0,
|
|
216
|
+
thumbs_up INTEGER DEFAULT 0,
|
|
217
|
+
thumbs_down INTEGER DEFAULT 0,
|
|
218
|
+
disabled INTEGER DEFAULT 0,
|
|
219
|
+
updated_at REAL,
|
|
220
|
+
PRIMARY KEY (user_id, skill_name)
|
|
221
|
+
);
|
|
222
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
223
|
+
id TEXT PRIMARY KEY,
|
|
224
|
+
user_id TEXT DEFAULT '',
|
|
225
|
+
created_at REAL,
|
|
226
|
+
active_skills TEXT,
|
|
227
|
+
turn_count INTEGER DEFAULT 0
|
|
228
|
+
);
|
|
229
|
+
CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts DESC);
|
|
230
|
+
CREATE INDEX IF NOT EXISTS idx_events_user ON events(user_id, ts DESC);
|
|
231
|
+
""")
|
|
232
|
+
# Backward-compat: if upgrading from 0.1.0 schema, add user_id column where missing.
|
|
233
|
+
for table in ("events", "sessions"):
|
|
234
|
+
try:
|
|
235
|
+
con.execute(f"ALTER TABLE {table} ADD COLUMN user_id TEXT DEFAULT ''")
|
|
236
|
+
except sqlite3.OperationalError:
|
|
237
|
+
pass # already exists
|
|
238
|
+
con.commit()
|
|
239
|
+
return con
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def log_event(con, session_id, event_type, payload, user_id=""):
|
|
243
|
+
con.execute(
|
|
244
|
+
"INSERT INTO events (id, ts, user_id, session_id, event_type, payload) VALUES (?, ?, ?, ?, ?, ?)",
|
|
245
|
+
(str(uuid.uuid4()), time.time(), user_id, session_id, event_type, json.dumps(payload)),
|
|
246
|
+
)
|
|
247
|
+
con.commit()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def get_skill_weight(con, name, user_id=""):
|
|
251
|
+
cur = con.execute(
|
|
252
|
+
"SELECT weight, disabled FROM skill_weights WHERE user_id = ? AND skill_name = ?",
|
|
253
|
+
(user_id, name),
|
|
254
|
+
)
|
|
255
|
+
row = cur.fetchone()
|
|
256
|
+
if not row:
|
|
257
|
+
return 0.0, False
|
|
258
|
+
return row[0], bool(row[1])
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def update_skill_stat(con, name, field, delta=1, user_id=""):
|
|
262
|
+
con.execute(
|
|
263
|
+
f"""INSERT INTO skill_weights (user_id, skill_name, {field}, updated_at)
|
|
264
|
+
VALUES (?, ?, ?, ?)
|
|
265
|
+
ON CONFLICT(user_id, skill_name) DO UPDATE SET
|
|
266
|
+
{field} = {field} + ?,
|
|
267
|
+
updated_at = ?""",
|
|
268
|
+
(user_id, name, delta, time.time(), delta, time.time()),
|
|
269
|
+
)
|
|
270
|
+
cur = con.execute(
|
|
271
|
+
"SELECT uses, referenced, thumbs_up, thumbs_down FROM skill_weights WHERE user_id = ? AND skill_name = ?",
|
|
272
|
+
(user_id, name),
|
|
273
|
+
)
|
|
274
|
+
row = cur.fetchone()
|
|
275
|
+
if row:
|
|
276
|
+
uses, referenced, up, down = row
|
|
277
|
+
ref_rate = (referenced / uses) if uses > 0 else 0.0
|
|
278
|
+
thumbs_score = (up - down) * 0.1
|
|
279
|
+
weight = (ref_rate - 0.5) * 0.3 + thumbs_score
|
|
280
|
+
con.execute(
|
|
281
|
+
"UPDATE skill_weights SET weight = ? WHERE user_id = ? AND skill_name = ?",
|
|
282
|
+
(weight, user_id, name),
|
|
283
|
+
)
|
|
284
|
+
con.commit()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def set_skill_disabled(con, name, disabled: bool, user_id=""):
|
|
288
|
+
con.execute(
|
|
289
|
+
"""INSERT INTO skill_weights (user_id, skill_name, disabled, updated_at) VALUES (?, ?, ?, ?)
|
|
290
|
+
ON CONFLICT(user_id, skill_name) DO UPDATE SET disabled = ?, updated_at = ?""",
|
|
291
|
+
(user_id, name, int(disabled), time.time(), int(disabled), time.time()),
|
|
292
|
+
)
|
|
293
|
+
con.commit()
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ---------- Router ----------
|
|
297
|
+
class Router:
|
|
298
|
+
def __init__(self, skills, embed_model, anthropic: Optional[AsyncAnthropic]):
|
|
299
|
+
self.skills = skills
|
|
300
|
+
self.embed_model = embed_model
|
|
301
|
+
self.anthropic = anthropic
|
|
302
|
+
texts = [f"{s.title}: {s.description}" for s in skills]
|
|
303
|
+
print(f"[skillforge] Embedding {len(skills)} skills...")
|
|
304
|
+
embeddings = embed_model.encode(texts, show_progress_bar=False, convert_to_numpy=True)
|
|
305
|
+
for s, e in zip(skills, embeddings):
|
|
306
|
+
s.embedding = e / np.linalg.norm(e)
|
|
307
|
+
self.matrix = np.stack([s.embedding for s in skills]) if skills else np.zeros((0, 0))
|
|
308
|
+
print(f"[skillforge] Ready. {len(skills)} skills, matrix shape: {self.matrix.shape}")
|
|
309
|
+
|
|
310
|
+
def shortlist(self, prompt, con, k=TOP_K_CANDIDATES, user_id=""):
|
|
311
|
+
if len(self.skills) == 0:
|
|
312
|
+
return []
|
|
313
|
+
q = self.embed_model.encode(prompt, convert_to_numpy=True)
|
|
314
|
+
q = q / np.linalg.norm(q)
|
|
315
|
+
sims = self.matrix @ q
|
|
316
|
+
biased = sims.copy()
|
|
317
|
+
for i, s in enumerate(self.skills):
|
|
318
|
+
w, disabled = get_skill_weight(con, s.name, user_id=user_id)
|
|
319
|
+
if disabled:
|
|
320
|
+
biased[i] = -999.0
|
|
321
|
+
else:
|
|
322
|
+
biased[i] += w
|
|
323
|
+
top_idx = np.argsort(-biased)[:k]
|
|
324
|
+
return [(self.skills[i], float(sims[i])) for i in top_idx if biased[i] > -100]
|
|
325
|
+
|
|
326
|
+
def pick_final_embedding_only(self, candidates):
|
|
327
|
+
"""Pick up to MAX_ACTIVE_SKILLS from the shortlist order (similarity + weights). No LLM call."""
|
|
328
|
+
if not candidates:
|
|
329
|
+
return [], "no candidates available"
|
|
330
|
+
names = [s.name for s, _ in candidates[:MAX_ACTIVE_SKILLS]]
|
|
331
|
+
return names, (
|
|
332
|
+
"embedding-only: top candidates by similarity and learned weights"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
async def pick_final(self, prompt, conversation, candidates):
|
|
336
|
+
if self.anthropic is None:
|
|
337
|
+
return self.pick_final_embedding_only(candidates)
|
|
338
|
+
if not candidates:
|
|
339
|
+
return [], "no candidates available"
|
|
340
|
+
catalog = "\n".join(
|
|
341
|
+
f"- {s.name}: {s.description[:200]}" for s, _ in candidates
|
|
342
|
+
)
|
|
343
|
+
recent = ""
|
|
344
|
+
if conversation:
|
|
345
|
+
recent = "\n\nRecent conversation:\n" + "\n".join(
|
|
346
|
+
f"{m['role']}: {m['content'][:200]}" for m in conversation[-4:]
|
|
347
|
+
)
|
|
348
|
+
sys = (
|
|
349
|
+
"You are a skill router. Given a user prompt and a candidate list of skills, "
|
|
350
|
+
f"pick 0 to {MAX_ACTIVE_SKILLS} skills that would genuinely help answer this prompt. "
|
|
351
|
+
"Be ruthless — only include a skill if it directly applies. Empty list is valid. "
|
|
352
|
+
'Respond ONLY in JSON: {"skills": ["name1","name2"], "reasoning": "one sentence"}'
|
|
353
|
+
)
|
|
354
|
+
user = f"User prompt:\n{prompt}{recent}\n\nCandidate skills:\n{catalog}"
|
|
355
|
+
try:
|
|
356
|
+
resp = await self.anthropic.messages.create(
|
|
357
|
+
model=ROUTER_MODEL,
|
|
358
|
+
max_tokens=400,
|
|
359
|
+
system=sys,
|
|
360
|
+
messages=[{"role": "user", "content": user}],
|
|
361
|
+
)
|
|
362
|
+
text = resp.content[0].text.strip()
|
|
363
|
+
if text.startswith("```"):
|
|
364
|
+
text = text.split("```")[1]
|
|
365
|
+
if text.startswith("json"):
|
|
366
|
+
text = text[4:]
|
|
367
|
+
data = json.loads(text.strip())
|
|
368
|
+
picked = [n for n in data.get("skills", []) if any(s.name == n for s, _ in candidates)]
|
|
369
|
+
return picked[:MAX_ACTIVE_SKILLS], data.get("reasoning", "")
|
|
370
|
+
except Exception as e:
|
|
371
|
+
return [s.name for s, _ in candidates[:3]], f"router-fallback: {e}"
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def jaccard_change(old, new):
|
|
375
|
+
if not old and not new:
|
|
376
|
+
return 0.0
|
|
377
|
+
if not old or not new:
|
|
378
|
+
return 1.0
|
|
379
|
+
inter = len(old & new)
|
|
380
|
+
union = len(old | new)
|
|
381
|
+
return 1.0 - (inter / union)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
async def run_route_turn(
|
|
385
|
+
con: sqlite3.Connection,
|
|
386
|
+
router: Router,
|
|
387
|
+
prompt: str,
|
|
388
|
+
conversation: list,
|
|
389
|
+
user_id: str = "",
|
|
390
|
+
session_id: str | None = None,
|
|
391
|
+
) -> dict[str, Any]:
|
|
392
|
+
"""Shared routing + session + telemetry for HTTP /chat and MCP route_skills.
|
|
393
|
+
|
|
394
|
+
Updates sessions, skill usage stats, and writes a route row to events.
|
|
395
|
+
"""
|
|
396
|
+
sid = session_id or str(uuid.uuid4())
|
|
397
|
+
t0 = time.time()
|
|
398
|
+
candidates = router.shortlist(prompt, con, user_id=user_id)
|
|
399
|
+
picked_names, reasoning = await router.pick_final(prompt, conversation, candidates)
|
|
400
|
+
route_ms = (time.time() - t0) * 1000
|
|
401
|
+
|
|
402
|
+
prev_active: set[str] = set()
|
|
403
|
+
cur = con.execute(
|
|
404
|
+
"SELECT active_skills FROM sessions WHERE id = ? AND user_id = ?",
|
|
405
|
+
(sid, user_id),
|
|
406
|
+
)
|
|
407
|
+
row = cur.fetchone()
|
|
408
|
+
if row and row[0]:
|
|
409
|
+
prev_active = set(json.loads(row[0]))
|
|
410
|
+
change = jaccard_change(prev_active, set(picked_names))
|
|
411
|
+
rerouted = change >= REROUTE_THRESHOLD and bool(prev_active)
|
|
412
|
+
|
|
413
|
+
con.execute(
|
|
414
|
+
"""INSERT INTO sessions (id, user_id, created_at, active_skills, turn_count) VALUES (?, ?, ?, ?, 1)
|
|
415
|
+
ON CONFLICT(id) DO UPDATE SET active_skills = ?, turn_count = turn_count + 1""",
|
|
416
|
+
(sid, user_id, time.time(), json.dumps(picked_names), json.dumps(picked_names)),
|
|
417
|
+
)
|
|
418
|
+
con.commit()
|
|
419
|
+
for n in picked_names:
|
|
420
|
+
update_skill_stat(con, n, "uses", 1, user_id=user_id)
|
|
421
|
+
|
|
422
|
+
event = {
|
|
423
|
+
"type": "route",
|
|
424
|
+
"session_id": sid,
|
|
425
|
+
"user_id": user_id,
|
|
426
|
+
"prompt": prompt[:300],
|
|
427
|
+
"candidates": [{"name": s.name, "score": sc} for s, sc in candidates[:10]],
|
|
428
|
+
"picked": picked_names,
|
|
429
|
+
"reasoning": reasoning,
|
|
430
|
+
"rerouted": rerouted,
|
|
431
|
+
"change_pct": round(change * 100, 1),
|
|
432
|
+
"route_ms": round(route_ms, 1),
|
|
433
|
+
"ts": time.time(),
|
|
434
|
+
}
|
|
435
|
+
log_event(con, sid, "route", event, user_id=user_id)
|
|
436
|
+
return {
|
|
437
|
+
"session_id": sid,
|
|
438
|
+
"picked_names": picked_names,
|
|
439
|
+
"reasoning": reasoning,
|
|
440
|
+
"candidates": candidates,
|
|
441
|
+
"route_ms": route_ms,
|
|
442
|
+
"rerouted": rerouted,
|
|
443
|
+
"change": change,
|
|
444
|
+
"event": event,
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# ---------- App ----------
|
|
449
|
+
app_state: dict[str, Any] = {}
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@asynccontextmanager
|
|
453
|
+
async def lifespan(app: FastAPI):
|
|
454
|
+
print(f"[skillforge] Loading skills from {BUNDLED_SKILLS} + {USER_SKILLS}")
|
|
455
|
+
skills = load_all_skills()
|
|
456
|
+
print(f"[skillforge] Loaded {len(skills)} skills")
|
|
457
|
+
if not skills:
|
|
458
|
+
print("[skillforge] WARNING: no skills found")
|
|
459
|
+
embed_model = SentenceTransformer(EMBED_MODEL)
|
|
460
|
+
anthropic = AsyncAnthropic()
|
|
461
|
+
router_anthropic = None if SKILLFORGE_ROUTER_MODE == "embedding" else anthropic
|
|
462
|
+
if router_anthropic is None:
|
|
463
|
+
print("[skillforge] Router mode: embedding-only (Haiku step skipped; /chat still uses ANSWER model)")
|
|
464
|
+
print("[skillforge] Live usage (terminal): skillforge events --watch")
|
|
465
|
+
router = Router(skills, embed_model, router_anthropic)
|
|
466
|
+
con = init_db()
|
|
467
|
+
app_state.update(
|
|
468
|
+
skills={s.name: s for s in skills},
|
|
469
|
+
router=router,
|
|
470
|
+
anthropic=anthropic,
|
|
471
|
+
con=con,
|
|
472
|
+
)
|
|
473
|
+
yield
|
|
474
|
+
con.close()
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
app = FastAPI(lifespan=lifespan, title="skillforge")
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class ChatRequest(BaseModel):
|
|
481
|
+
prompt: str
|
|
482
|
+
session_id: str | None = None
|
|
483
|
+
conversation: list[dict] = []
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class FeedbackRequest(BaseModel):
|
|
487
|
+
session_id: str
|
|
488
|
+
skill_name: str
|
|
489
|
+
thumbs: int
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class DisableRequest(BaseModel):
|
|
493
|
+
skill_name: str
|
|
494
|
+
disabled: bool
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@app.post("/chat")
|
|
498
|
+
async def chat(req: ChatRequest, request: Request):
|
|
499
|
+
from app.auth import resolve_user
|
|
500
|
+
user_id = resolve_user(request)
|
|
501
|
+
router: Router = app_state["router"]
|
|
502
|
+
con = app_state["con"]
|
|
503
|
+
anthropic: AsyncAnthropic = app_state["anthropic"]
|
|
504
|
+
|
|
505
|
+
result = await run_route_turn(
|
|
506
|
+
con,
|
|
507
|
+
router,
|
|
508
|
+
req.prompt,
|
|
509
|
+
req.conversation,
|
|
510
|
+
user_id=user_id,
|
|
511
|
+
session_id=req.session_id,
|
|
512
|
+
)
|
|
513
|
+
session_id = result["session_id"]
|
|
514
|
+
picked_names = result["picked_names"]
|
|
515
|
+
|
|
516
|
+
skills_map = app_state["skills"]
|
|
517
|
+
skill_blocks = []
|
|
518
|
+
for n in picked_names:
|
|
519
|
+
s = skills_map.get(n)
|
|
520
|
+
if s:
|
|
521
|
+
skill_blocks.append(f'<skill name="{s.name}">\n{s.body}\n</skill>')
|
|
522
|
+
system_prompt = (
|
|
523
|
+
"You are a helpful assistant. The following skills have been dynamically loaded "
|
|
524
|
+
"for this turn based on the user's request. Use them when relevant; ignore them when not.\n\n"
|
|
525
|
+
+ "\n\n".join(skill_blocks)
|
|
526
|
+
) if skill_blocks else "You are a helpful assistant."
|
|
527
|
+
|
|
528
|
+
messages = req.conversation + [{"role": "user", "content": req.prompt}]
|
|
529
|
+
|
|
530
|
+
async def stream():
|
|
531
|
+
full_text = []
|
|
532
|
+
try:
|
|
533
|
+
async with anthropic.messages.stream(
|
|
534
|
+
model=ANSWER_MODEL,
|
|
535
|
+
max_tokens=4096,
|
|
536
|
+
system=system_prompt,
|
|
537
|
+
messages=messages,
|
|
538
|
+
) as s:
|
|
539
|
+
async for chunk in s.text_stream:
|
|
540
|
+
full_text.append(chunk)
|
|
541
|
+
yield f"data: {json.dumps({'delta': chunk})}\n\n"
|
|
542
|
+
except Exception as e:
|
|
543
|
+
yield f"data: {json.dumps({'error': str(e)})}\n\n"
|
|
544
|
+
return
|
|
545
|
+
response_text = "".join(full_text)
|
|
546
|
+
for n in picked_names:
|
|
547
|
+
s = skills_map.get(n)
|
|
548
|
+
if not s:
|
|
549
|
+
continue
|
|
550
|
+
keywords = [w for w in s.body.split()[:50] if len(w) > 6][:5]
|
|
551
|
+
hits = sum(1 for kw in keywords if kw.lower() in response_text.lower())
|
|
552
|
+
if hits >= 2 or s.name in response_text.lower():
|
|
553
|
+
update_skill_stat(con, n, "referenced", 1, user_id=user_id)
|
|
554
|
+
yield f"data: {json.dumps({'done': True, 'session_id': session_id, 'picked': picked_names})}\n\n"
|
|
555
|
+
|
|
556
|
+
return StreamingResponse(stream(), media_type="text/event-stream")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
@app.post("/feedback")
|
|
560
|
+
def feedback(req: FeedbackRequest, request: Request):
|
|
561
|
+
from app.auth import resolve_user
|
|
562
|
+
user_id = resolve_user(request)
|
|
563
|
+
con = app_state["con"]
|
|
564
|
+
field = "thumbs_up" if req.thumbs > 0 else "thumbs_down"
|
|
565
|
+
update_skill_stat(con, req.skill_name, field, 1, user_id=user_id)
|
|
566
|
+
log_event(con, req.session_id, "feedback",
|
|
567
|
+
{"skill": req.skill_name, "thumbs": req.thumbs},
|
|
568
|
+
user_id=user_id)
|
|
569
|
+
return {"ok": True}
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
@app.post("/skills/disable")
|
|
573
|
+
def disable(req: DisableRequest, request: Request):
|
|
574
|
+
from app.auth import resolve_user
|
|
575
|
+
user_id = resolve_user(request)
|
|
576
|
+
con = app_state["con"]
|
|
577
|
+
set_skill_disabled(con, req.skill_name, req.disabled, user_id=user_id)
|
|
578
|
+
return {"ok": True}
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@app.get("/skills")
|
|
582
|
+
def list_skills(request: Request):
|
|
583
|
+
from app.auth import resolve_user
|
|
584
|
+
user_id = resolve_user(request)
|
|
585
|
+
con = app_state["con"]
|
|
586
|
+
skills_map = app_state["skills"]
|
|
587
|
+
out = []
|
|
588
|
+
for name, s in skills_map.items():
|
|
589
|
+
cur = con.execute(
|
|
590
|
+
"SELECT weight, uses, referenced, thumbs_up, thumbs_down, disabled FROM skill_weights WHERE user_id = ? AND skill_name = ?",
|
|
591
|
+
(user_id, name),
|
|
592
|
+
)
|
|
593
|
+
row = cur.fetchone()
|
|
594
|
+
weight, uses, ref, up, down, disabled = row if row else (0.0, 0, 0, 0, 0, 0)
|
|
595
|
+
out.append({
|
|
596
|
+
"name": name,
|
|
597
|
+
"title": s.title,
|
|
598
|
+
"description": s.description[:200],
|
|
599
|
+
"source": s.source,
|
|
600
|
+
"weight": weight,
|
|
601
|
+
"uses": uses,
|
|
602
|
+
"referenced": ref,
|
|
603
|
+
"thumbs_up": up,
|
|
604
|
+
"thumbs_down": down,
|
|
605
|
+
"disabled": bool(disabled),
|
|
606
|
+
})
|
|
607
|
+
out.sort(key=lambda x: -x["uses"])
|
|
608
|
+
return out
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@app.get("/events")
|
|
612
|
+
def recent_events(request: Request, limit: int = 50):
|
|
613
|
+
from app.auth import resolve_user, auth_enabled
|
|
614
|
+
user_id = resolve_user(request)
|
|
615
|
+
con = app_state["con"]
|
|
616
|
+
if auth_enabled():
|
|
617
|
+
cur = con.execute(
|
|
618
|
+
"SELECT ts, session_id, event_type, payload FROM events WHERE user_id = ? ORDER BY ts DESC LIMIT ?",
|
|
619
|
+
(user_id, limit),
|
|
620
|
+
)
|
|
621
|
+
else:
|
|
622
|
+
cur = con.execute(
|
|
623
|
+
"SELECT ts, session_id, event_type, payload FROM events ORDER BY ts DESC LIMIT ?",
|
|
624
|
+
(limit,),
|
|
625
|
+
)
|
|
626
|
+
return [
|
|
627
|
+
{"ts": ts, "session_id": sid, "type": et, "payload": json.loads(p)}
|
|
628
|
+
for ts, sid, et, p in cur.fetchall()
|
|
629
|
+
]
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
@app.get("/")
|
|
633
|
+
def root():
|
|
634
|
+
return {
|
|
635
|
+
"service": "skillforge",
|
|
636
|
+
"docs": "POST /chat, GET /events, GET /skills, GET /healthz",
|
|
637
|
+
"live_log": "skillforge events --watch",
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
@app.get("/healthz")
|
|
642
|
+
def health():
|
|
643
|
+
return {
|
|
644
|
+
"skills_loaded": len(app_state.get("skills", {})),
|
|
645
|
+
"ok": True,
|
|
646
|
+
"live_log": "skillforge events --watch",
|
|
647
|
+
}
|