codeply-cli 0.5.6 → 0.6.0
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/LICENSE +21 -21
- package/THIRD_PARTY_NOTICES.md +65 -65
- package/bin/codeply.js +8 -3
- package/lib/agent.mjs +2596 -2268
- package/lib/ai.js +135 -27
- package/lib/ai.mjs +6 -6
- package/lib/apply-patch.js +217 -217
- package/lib/arity-table.json +138 -138
- package/lib/arity.js +70 -70
- package/lib/auth.mjs +6 -6
- package/lib/autopilot.js +84 -0
- package/lib/bot-mail.js +46 -0
- package/lib/bots.js +142 -15
- package/lib/brain.js +914 -0
- package/lib/call-assist.js +120 -0
- package/lib/chatgpt.js +282 -282
- package/lib/cloud-browser.mjs +143 -143
- package/lib/cloud.mjs +1396 -1320
- package/lib/commands.js +103 -103
- package/lib/computer.js +136 -0
- package/lib/design-library/assemble-artifact.mjs +21 -21
- package/lib/design-library/cleanup.mjs +45 -45
- package/lib/design-library/index.json +22379 -22379
- package/lib/design-library/make-table-data.mjs +39 -39
- package/lib/design-tools.mjs +173 -0
- package/lib/diagnostics.js +192 -192
- package/lib/dsh-acp.mjs +444 -0
- package/lib/edit-engine.js +364 -364
- package/lib/github-agent.mjs +326 -326
- package/lib/handoff.mjs +245 -0
- package/lib/history.js +78 -75
- package/lib/life-sources.js +319 -0
- package/lib/life-store.js +356 -0
- package/lib/life-triggers.js +286 -0
- package/lib/life.js +487 -0
- package/lib/mail-watch.js +3 -3
- package/lib/mcp.js +258 -258
- package/lib/native-tools.mjs +17 -5
- package/lib/oauth-connectors.js +76 -21
- package/lib/permissions.js +109 -109
- package/lib/plugins.js +356 -353
- package/lib/reach.js +67 -67
- package/lib/render-md.mjs +647 -617
- package/lib/screen.js +77 -0
- package/lib/server.mjs +458 -457
- package/lib/session-db.js +220 -220
- package/lib/share.js +208 -168
- package/lib/skills.js +4 -1
- package/lib/snapshot.js +146 -146
- package/lib/subagents.js +4 -2
- package/lib/telemetry.js +105 -105
- package/lib/terminal-hints.js +154 -154
- package/lib/textarea.mjs +264 -258
- package/lib/tools.mjs +326 -40
- package/lib/tui.mjs +1459 -1086
- package/lib/typecheck-worker.js +147 -147
- package/lib/web-tools.js +209 -209
- package/package.json +1 -1
- package/skills/SOURCE.md +58 -58
- package/skills/accessibility/SKILL.md +147 -147
- package/skills/agent-architecture-audit/SKILL.md +257 -257
- package/skills/agent-eval/SKILL.md +147 -147
- package/skills/agent-harness-construction/SKILL.md +74 -74
- package/skills/agent-introspection-debugging/SKILL.md +154 -154
- package/skills/agent-payment-x402/SKILL.md +225 -225
- package/skills/agent-self-evaluation/SKILL.md +182 -182
- package/skills/agent-self-evaluation/references/evaluation-criteria.md +71 -71
- package/skills/agent-self-evaluation/references/hook-integration.md +64 -64
- package/skills/agent-sort/SKILL.md +216 -216
- package/skills/agentic-engineering/SKILL.md +64 -64
- package/skills/agentic-os/SKILL.md +388 -388
- package/skills/ai-first-engineering/SKILL.md +52 -52
- package/skills/ai-regression-testing/SKILL.md +386 -386
- package/skills/android-clean-architecture/SKILL.md +340 -340
- package/skills/angular-developer/SKILL.md +155 -155
- package/skills/angular-developer/references/angular-animations.md +160 -160
- package/skills/angular-developer/references/angular-aria.md +410 -410
- package/skills/angular-developer/references/cli.md +86 -86
- package/skills/angular-developer/references/component-harnesses.md +59 -59
- package/skills/angular-developer/references/component-styling.md +91 -91
- package/skills/angular-developer/references/components.md +117 -117
- package/skills/angular-developer/references/creating-services.md +97 -97
- package/skills/angular-developer/references/data-resolvers.md +69 -69
- package/skills/angular-developer/references/define-routes.md +67 -67
- package/skills/angular-developer/references/defining-providers.md +72 -72
- package/skills/angular-developer/references/di-fundamentals.md +120 -120
- package/skills/angular-developer/references/e2e-testing.md +56 -56
- package/skills/angular-developer/references/effects.md +83 -83
- package/skills/angular-developer/references/hierarchical-injectors.md +43 -43
- package/skills/angular-developer/references/host-elements.md +80 -80
- package/skills/angular-developer/references/injection-context.md +63 -63
- package/skills/angular-developer/references/inputs.md +101 -101
- package/skills/angular-developer/references/linked-signal.md +59 -59
- package/skills/angular-developer/references/loading-strategies.md +61 -61
- package/skills/angular-developer/references/mcp.md +108 -108
- package/skills/angular-developer/references/navigate-to-routes.md +69 -69
- package/skills/angular-developer/references/outputs.md +86 -86
- package/skills/angular-developer/references/reactive-forms.md +122 -122
- package/skills/angular-developer/references/rendering-strategies.md +44 -44
- package/skills/angular-developer/references/resource.md +77 -77
- package/skills/angular-developer/references/route-animations.md +56 -56
- package/skills/angular-developer/references/route-guards.md +52 -52
- package/skills/angular-developer/references/router-lifecycle.md +45 -45
- package/skills/angular-developer/references/router-testing.md +87 -87
- package/skills/angular-developer/references/show-routes-with-outlets.md +68 -68
- package/skills/angular-developer/references/signal-forms.md +795 -795
- package/skills/angular-developer/references/signals-overview.md +94 -94
- package/skills/angular-developer/references/tailwind-css.md +69 -69
- package/skills/angular-developer/references/template-driven-forms.md +114 -114
- package/skills/angular-developer/references/testing-fundamentals.md +65 -65
- package/skills/api-connector-builder/SKILL.md +121 -121
- package/skills/api-design/SKILL.md +524 -524
- package/skills/architecture-decision-records/SKILL.md +180 -180
- package/skills/article-writing/SKILL.md +80 -80
- package/skills/automation-audit-ops/SKILL.md +143 -143
- package/skills/autonomous-agent-harness/SKILL.md +274 -274
- package/skills/autonomous-loops/SKILL.md +611 -611
- package/skills/backend-patterns/SKILL.md +562 -562
- package/skills/benchmark/SKILL.md +95 -95
- package/skills/benchmark-methodology/SKILL.md +191 -191
- package/skills/benchmark-optimization-loop/SKILL.md +71 -71
- package/skills/blender-motion-state-inspection/SKILL.md +165 -165
- package/skills/blueprint/SKILL.md +106 -106
- package/skills/brand-discovery/SKILL.md +145 -145
- package/skills/brand-discovery/references/10_purpose-why.md +40 -40
- package/skills/brand-discovery/references/20_positioning.md +44 -44
- package/skills/brand-discovery/references/30_audience-niche.md +52 -52
- package/skills/brand-discovery/references/40_personality-archetype.md +57 -57
- package/skills/brand-discovery/references/50_voice-tone.md +59 -59
- package/skills/brand-discovery/references/60_narrative-story.md +50 -50
- package/skills/brand-discovery/references/70_founder-tension.md +49 -49
- package/skills/brand-discovery/references/90_SYNTHESIS.md +133 -133
- package/skills/brand-voice/SKILL.md +98 -98
- package/skills/brand-voice/references/voice-profile-schema.md +55 -55
- package/skills/browser-qa/SKILL.md +105 -105
- package/skills/bun-runtime/SKILL.md +85 -85
- package/skills/canary-watch/SKILL.md +108 -108
- package/skills/carrier-relationship-management/SKILL.md +212 -212
- package/skills/cisco-ios-patterns/SKILL.md +164 -164
- package/skills/ck/SKILL.md +148 -148
- package/skills/claude-devfleet/SKILL.md +112 -112
- package/skills/click-path-audit/SKILL.md +245 -245
- package/skills/clickhouse-io/SKILL.md +445 -445
- package/skills/code-tour/SKILL.md +254 -254
- package/skills/codebase-onboarding/SKILL.md +234 -234
- package/skills/codehealth-mcp/SKILL.md +167 -167
- package/skills/codeply-taste/LICENSE +21 -0
- package/skills/codeply-taste/SKILL.md +100 -0
- package/skills/codeply-taste/SOURCE.md +10 -0
- package/skills/codeply-taste/assets/base.css +100 -0
- package/skills/codeply-taste/assets/base.js +37 -0
- package/skills/codeply-taste/library/directions.json +1078 -0
- package/skills/codeply-taste/library/heroes.json +894 -0
- package/skills/codeply-taste/library/motions.json +57 -0
- package/skills/codeply-taste/library/palettes.json +1485 -0
- package/skills/codeply-taste/library/photos.json +7240 -0
- package/skills/codeply-taste/library/sections.json +4136 -0
- package/skills/codeply-taste/library/sites.json +7274 -0
- package/skills/codeply-taste/library/type.json +2024 -0
- package/skills/codeply-taste/references/colour.md +141 -0
- package/skills/codeply-taste/references/copy.md +177 -0
- package/skills/codeply-taste/references/critique.md +171 -0
- package/skills/codeply-taste/references/imagery.md +377 -0
- package/skills/codeply-taste/references/layout.md +178 -0
- package/skills/codeply-taste/references/motion.md +292 -0
- package/skills/codeply-taste/references/research.md +306 -0
- package/skills/codeply-taste/references/tells.md +115 -0
- package/skills/codeply-taste/references/type.md +170 -0
- package/skills/codeply-taste/scripts/assets.mjs +865 -0
- package/skills/codeply-taste/scripts/draw.mjs +525 -0
- package/skills/codeply-taste/scripts/inspect.mjs +205 -0
- package/skills/codeply-taste/scripts/lib/cdp.mjs +205 -0
- package/skills/codeply-taste/scripts/lib/probe.mjs +569 -0
- package/skills/codeply-taste/scripts/lib/rules.mjs +137 -0
- package/skills/codeply-taste/scripts/lint.mjs +108 -0
- package/skills/coding-standards/SKILL.md +551 -551
- package/skills/competitive-platform-analysis/SKILL.md +214 -214
- package/skills/competitive-report-structure/SKILL.md +162 -162
- package/skills/compose-multiplatform-patterns/SKILL.md +300 -300
- package/skills/config-gc/SKILL.md +120 -120
- package/skills/configure-ecc/SKILL.md +385 -385
- package/skills/connections-optimizer/SKILL.md +190 -190
- package/skills/content-engine/SKILL.md +132 -132
- package/skills/content-hash-cache-pattern/SKILL.md +162 -162
- package/skills/context-budget/SKILL.md +136 -136
- package/skills/continuous-agent-loop/SKILL.md +46 -46
- package/skills/continuous-learning/SKILL.md +132 -132
- package/skills/continuous-learning-v2/SKILL.md +361 -361
- package/skills/contract-first/SKILL.md +287 -287
- package/skills/cost-aware-llm-pipeline/SKILL.md +184 -184
- package/skills/cost-tracking/SKILL.md +97 -97
- package/skills/council/SKILL.md +204 -204
- package/skills/cpp-coding-standards/SKILL.md +724 -724
- package/skills/cpp-testing/SKILL.md +325 -325
- package/skills/crosspost/SKILL.md +112 -112
- package/skills/csharp-testing/SKILL.md +322 -322
- package/skills/customer-billing-ops/SKILL.md +141 -141
- package/skills/customs-trade-compliance/SKILL.md +263 -263
- package/skills/dart-flutter-patterns/SKILL.md +564 -564
- package/skills/dashboard-builder/SKILL.md +109 -109
- package/skills/data-scraper-agent/SKILL.md +765 -765
- package/skills/data-throughput-accelerator/SKILL.md +74 -74
- package/skills/database-migrations/SKILL.md +430 -430
- package/skills/deep-research/SKILL.md +160 -160
- package/skills/defi-amm-security/SKILL.md +167 -167
- package/skills/delivery-gate/SKILL.md +126 -126
- package/skills/deployment-patterns/SKILL.md +428 -428
- package/skills/design-system/SKILL.md +83 -83
- package/skills/django-celery/SKILL.md +458 -458
- package/skills/django-patterns/SKILL.md +735 -735
- package/skills/django-security/SKILL.md +644 -644
- package/skills/django-tdd/SKILL.md +730 -730
- package/skills/django-verification/SKILL.md +470 -470
- package/skills/dmux-workflows/SKILL.md +192 -192
- package/skills/docker-patterns/SKILL.md +365 -365
- package/skills/documentation-lookup/SKILL.md +91 -91
- package/skills/dotnet-patterns/SKILL.md +322 -322
- package/skills/dynamic-workflow-mode/SKILL.md +124 -124
- package/skills/e2e-testing/SKILL.md +327 -327
- package/skills/ecc-guide/SKILL.md +190 -190
- package/skills/ecc-recipes/SKILL.md +149 -149
- package/skills/ecc-tools-cost-audit/SKILL.md +161 -161
- package/skills/email-ops/SKILL.md +122 -122
- package/skills/energy-procurement/SKILL.md +228 -228
- package/skills/enterprise-agent-ops/SKILL.md +51 -51
- package/skills/error-handling/SKILL.md +377 -377
- package/skills/eval-harness/SKILL.md +271 -271
- package/skills/evm-token-decimals/SKILL.md +131 -131
- package/skills/exa-search/SKILL.md +108 -108
- package/skills/fal-ai-media/SKILL.md +289 -289
- package/skills/fastapi-patterns/SKILL.md +514 -514
- package/skills/finance-billing-ops/SKILL.md +128 -128
- package/skills/flox-environments/SKILL.md +497 -497
- package/skills/flutter-dart-code-review/SKILL.md +436 -436
- package/skills/foundation-models-on-device/SKILL.md +243 -243
- package/skills/frontend-a11y/SKILL.md +446 -446
- package/skills/frontend-design-direction/SKILL.md +93 -93
- package/skills/frontend-patterns/SKILL.md +657 -657
- package/skills/frontend-slides/SKILL.md +185 -185
- package/skills/fsharp-testing/SKILL.md +281 -281
- package/skills/gan-style-harness/SKILL.md +279 -279
- package/skills/gateguard/SKILL.md +133 -133
- package/skills/generating-python-installer/SKILL.md +820 -820
- package/skills/git-workflow/SKILL.md +716 -716
- package/skills/github-ops/SKILL.md +145 -145
- package/skills/golang-patterns/SKILL.md +676 -676
- package/skills/golang-testing/SKILL.md +721 -721
- package/skills/google-workspace-ops/SKILL.md +96 -96
- package/skills/growth-log/SKILL.md +128 -128
- package/skills/healthcare-cdss-patterns/SKILL.md +246 -246
- package/skills/healthcare-emr-patterns/SKILL.md +160 -160
- package/skills/healthcare-eval-harness/SKILL.md +208 -208
- package/skills/healthcare-phi-compliance/SKILL.md +146 -146
- package/skills/hermes-imports/SKILL.md +89 -89
- package/skills/hexagonal-architecture/SKILL.md +277 -277
- package/skills/hipaa-compliance/SKILL.md +79 -79
- package/skills/homelab-network-readiness/SKILL.md +170 -170
- package/skills/homelab-network-setup/SKILL.md +130 -130
- package/skills/homelab-pihole-dns/SKILL.md +275 -275
- package/skills/homelab-vlan-segmentation/SKILL.md +312 -312
- package/skills/homelab-wireguard-vpn/SKILL.md +306 -306
- package/skills/hookify-rules/SKILL.md +128 -128
- package/skills/inherit-legacy-style/SKILL.md +157 -157
- package/skills/intent-driven-development/SKILL.md +360 -360
- package/skills/inventory-demand-planning/SKILL.md +247 -247
- package/skills/investor-materials/SKILL.md +97 -97
- package/skills/investor-outreach/SKILL.md +92 -92
- package/skills/ios-icon-gen/SKILL.md +158 -158
- package/skills/iterative-retrieval/SKILL.md +212 -212
- package/skills/ito-basket-compare/SKILL.md +64 -64
- package/skills/ito-compute/SKILL.md +129 -129
- package/skills/ito-data-atlas-agent/SKILL.md +64 -64
- package/skills/ito-market-intelligence/SKILL.md +61 -61
- package/skills/ito-trade-planner/SKILL.md +68 -68
- package/skills/java-coding-standards/SKILL.md +384 -384
- package/skills/jira-integration/SKILL.md +303 -303
- package/skills/jpa-patterns/SKILL.md +152 -152
- package/skills/knowledge-ops/SKILL.md +155 -155
- package/skills/kotlin-coroutines-flows/SKILL.md +285 -285
- package/skills/kotlin-exposed-patterns/SKILL.md +720 -720
- package/skills/kotlin-ktor-patterns/SKILL.md +690 -690
- package/skills/kotlin-patterns/SKILL.md +712 -712
- package/skills/kotlin-testing/SKILL.md +825 -825
- package/skills/kubernetes-patterns/SKILL.md +756 -756
- package/skills/laravel-patterns/SKILL.md +416 -416
- package/skills/laravel-plugin-discovery/SKILL.md +230 -230
- package/skills/laravel-security/SKILL.md +948 -948
- package/skills/laravel-tdd/SKILL.md +675 -675
- package/skills/laravel-verification/SKILL.md +180 -180
- package/skills/latency-critical-systems/SKILL.md +75 -75
- package/skills/lead-intelligence/SKILL.md +322 -322
- package/skills/liquid-glass-design/SKILL.md +279 -279
- package/skills/llm-trading-agent-security/SKILL.md +147 -147
- package/skills/logistics-exception-management/SKILL.md +222 -222
- package/skills/loop-design-check/SKILL.md +143 -143
- package/skills/mailtrap-email-integration/SKILL.md +77 -77
- package/skills/make-interfaces-feel-better/SKILL.md +152 -152
- package/skills/manim-video/SKILL.md +90 -90
- package/skills/market-research/SKILL.md +76 -76
- package/skills/marketing-campaign/SKILL.md +114 -114
- package/skills/mcp-server-patterns/SKILL.md +70 -70
- package/skills/messages-ops/SKILL.md +105 -105
- package/skills/ml-adoption-playbook/SKILL.md +57 -57
- package/skills/mle-workflow/SKILL.md +348 -348
- package/skills/motion-advanced/SKILL.md +596 -596
- package/skills/motion-foundations/SKILL.md +299 -299
- package/skills/motion-patterns/SKILL.md +434 -434
- package/skills/motion-ui/SKILL.md +576 -576
- package/skills/mysql-patterns/SKILL.md +413 -413
- package/skills/nanoclaw-repl/SKILL.md +34 -34
- package/skills/nestjs-patterns/SKILL.md +231 -231
- package/skills/netmiko-ssh-automation/SKILL.md +174 -174
- package/skills/network-bgp-diagnostics/SKILL.md +168 -168
- package/skills/network-config-validation/SKILL.md +211 -211
- package/skills/network-interface-health/SKILL.md +153 -153
- package/skills/nextjs-turbopack/SKILL.md +58 -58
- package/skills/nodejs-keccak256/SKILL.md +103 -103
- package/skills/nutrient-document-processing/SKILL.md +168 -168
- package/skills/nuxt4-patterns/SKILL.md +101 -101
- package/skills/openclaw-persona-forge/SKILL.md +289 -289
- package/skills/openclaw-persona-forge/references/avatar-style.md +124 -124
- package/skills/openclaw-persona-forge/references/boundary-rules.md +53 -53
- package/skills/openclaw-persona-forge/references/error-handling.md +53 -53
- package/skills/openclaw-persona-forge/references/identity-tension.md +48 -48
- package/skills/openclaw-persona-forge/references/naming-system.md +39 -39
- package/skills/openclaw-persona-forge/references/output-template.md +166 -166
- package/skills/opensource-pipeline/SKILL.md +256 -256
- package/skills/orch-add-feature/SKILL.md +45 -45
- package/skills/orch-build-mvp/SKILL.md +49 -49
- package/skills/orch-change-feature/SKILL.md +43 -43
- package/skills/orch-fix-defect/SKILL.md +43 -43
- package/skills/orch-pipeline/SKILL.md +121 -121
- package/skills/orch-refine-code/SKILL.md +44 -44
- package/skills/parallel-execution-optimizer/SKILL.md +74 -74
- package/skills/perl-patterns/SKILL.md +505 -505
- package/skills/perl-security/SKILL.md +504 -504
- package/skills/perl-testing/SKILL.md +476 -476
- package/skills/plan-canvas/SKILL.md +153 -153
- package/skills/plan-orchestrate/SKILL.md +263 -263
- package/skills/plankton-code-quality/SKILL.md +237 -237
- package/skills/postgres-patterns/SKILL.md +148 -148
- package/skills/prediction-market-oracle-research/SKILL.md +64 -64
- package/skills/prediction-market-risk-review/SKILL.md +61 -61
- package/skills/premium-web-design/SKILL.md +154 -154
- package/skills/prisma-patterns/SKILL.md +401 -401
- package/skills/product-capability/SKILL.md +142 -142
- package/skills/product-lens/SKILL.md +93 -93
- package/skills/production-audit/SKILL.md +207 -207
- package/skills/production-scheduling/SKILL.md +238 -238
- package/skills/project-flow-ops/SKILL.md +112 -112
- package/skills/prompt-optimizer/SKILL.md +398 -398
- package/skills/python-patterns/SKILL.md +751 -751
- package/skills/python-testing/SKILL.md +817 -817
- package/skills/pytorch-patterns/SKILL.md +397 -397
- package/skills/quality-nonconformance/SKILL.md +260 -260
- package/skills/quarkus-patterns/SKILL.md +723 -723
- package/skills/quarkus-security/SKILL.md +468 -468
- package/skills/quarkus-tdd/SKILL.md +812 -812
- package/skills/quarkus-verification/SKILL.md +480 -480
- package/skills/ralphinho-rfc-pipeline/SKILL.md +68 -68
- package/skills/react-native-patterns/SKILL.md +326 -326
- package/skills/react-patterns/SKILL.md +342 -342
- package/skills/react-performance/SKILL.md +575 -575
- package/skills/react-testing/SKILL.md +424 -424
- package/skills/recsys-pipeline-architect/SKILL.md +115 -115
- package/skills/recursive-decision-ledger/SKILL.md +81 -81
- package/skills/redis-patterns/SKILL.md +404 -404
- package/skills/regex-vs-llm-structured-text/SKILL.md +221 -221
- package/skills/remotion-video-creation/SKILL.md +43 -43
- package/skills/repo-scan/SKILL.md +79 -79
- package/skills/research-ops/SKILL.md +113 -113
- package/skills/returns-reverse-logistics/SKILL.md +240 -240
- package/skills/rules-distill/SKILL.md +265 -265
- package/skills/rust-patterns/SKILL.md +500 -500
- package/skills/rust-testing/SKILL.md +501 -501
- package/skills/safety-guard/SKILL.md +76 -76
- package/skills/santa-method/SKILL.md +307 -307
- package/skills/scientific-db-pubmed-database/SKILL.md +176 -176
- package/skills/scientific-db-uspto-database/SKILL.md +178 -178
- package/skills/scientific-pkg-gget/SKILL.md +167 -167
- package/skills/scientific-thinking-literature-review/SKILL.md +193 -193
- package/skills/scientific-thinking-scholar-evaluation/SKILL.md +161 -161
- package/skills/search-first/SKILL.md +183 -183
- package/skills/security-bounty-hunter/SKILL.md +100 -100
- package/skills/security-review/SKILL.md +504 -504
- package/skills/security-scan/SKILL.md +166 -166
- package/skills/seo/SKILL.md +155 -155
- package/skills/skill-comply/SKILL.md +59 -59
- package/skills/skill-scout/SKILL.md +141 -141
- package/skills/skill-stocktake/SKILL.md +195 -195
- package/skills/social-graph-ranker/SKILL.md +155 -155
- package/skills/social-publisher/SKILL.md +130 -130
- package/skills/springboot-patterns/SKILL.md +315 -315
- package/skills/springboot-security/SKILL.md +273 -273
- package/skills/springboot-tdd/SKILL.md +159 -159
- package/skills/springboot-verification/SKILL.md +232 -232
- package/skills/strategic-compact/SKILL.md +142 -142
- package/skills/swift-actor-persistence/SKILL.md +144 -144
- package/skills/swift-concurrency-6-2/SKILL.md +216 -216
- package/skills/swift-protocol-di-testing/SKILL.md +191 -191
- package/skills/swiftui-patterns/SKILL.md +259 -259
- package/skills/taste/SKILL.md +264 -264
- package/skills/taste/references/genre-taxonomy.md +87 -87
- package/skills/tdd-workflow/SKILL.md +583 -583
- package/skills/team-agent-orchestration/SKILL.md +111 -111
- package/skills/team-builder/SKILL.md +169 -169
- package/skills/terminal-ops/SKILL.md +110 -110
- package/skills/tinystruct-patterns/SKILL.md +279 -279
- package/skills/tinystruct-patterns/references/architecture.md +90 -90
- package/skills/tinystruct-patterns/references/data-handling.md +60 -60
- package/skills/tinystruct-patterns/references/database.md +99 -99
- package/skills/tinystruct-patterns/references/routing.md +64 -64
- package/skills/tinystruct-patterns/references/system-usage.md +97 -97
- package/skills/tinystruct-patterns/references/testing.md +72 -72
- package/skills/token-budget-advisor/SKILL.md +134 -134
- package/skills/ui-demo/SKILL.md +466 -466
- package/skills/ui-to-vue/SKILL.md +135 -135
- package/skills/uncloud/SKILL.md +344 -344
- package/skills/unified-memory/SKILL.md +170 -170
- package/skills/unified-notifications-ops/SKILL.md +188 -188
- package/skills/verification-loop/SKILL.md +129 -129
- package/skills/video-editing/SKILL.md +311 -311
- package/skills/videodb/SKILL.md +375 -375
- package/skills/visa-doc-translate/SKILL.md +117 -117
- package/skills/vite-patterns/SKILL.md +450 -450
- package/skills/vue-patterns/SKILL.md +471 -471
- package/skills/windows-desktop-e2e/SKILL.md +888 -888
- package/skills/workspace-surface-audit/SKILL.md +126 -126
- package/skills/x-api/SKILL.md +235 -235
package/lib/brain.js
ADDED
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The brain: one personal context that every Codeply product and every bot
|
|
3
|
+
* shares, so the user never has to explain themselves twice. Craft, Crew,
|
|
4
|
+
* the autopilot, the CLI and `codeply serve` (the phone and the cloud) all
|
|
5
|
+
* run the same agent (agent.mjs), and every run reads this store.
|
|
6
|
+
*
|
|
7
|
+
* memories What Codeply has learned, in three layers:
|
|
8
|
+
* personal preferences, facts, routines, how they like to work
|
|
9
|
+
* (scope "me")
|
|
10
|
+
* project decisions, ideas, requirements, to-dos for one
|
|
11
|
+
* project (scope "project:<key>"), so a startup idea
|
|
12
|
+
* talked through in Crew is already known in Craft
|
|
13
|
+
* live temporary context ("in a meeting until 5", "on a
|
|
14
|
+
* trip this week") that expires by itself
|
|
15
|
+
* plus lessons the bots learned on the job and shared with the
|
|
16
|
+
* team (scope "team").
|
|
17
|
+
* changes Append-only: what was learned, reinforced, replaced, ignored,
|
|
18
|
+
* corrected or forgotten, when, and from where.
|
|
19
|
+
* projects The projects the brain knows by name (Craft registers every
|
|
20
|
+
* folder it runs in; chats in Crew are matched against them).
|
|
21
|
+
* handoffs The coordination board: one agent passes the part of a job
|
|
22
|
+
* that belongs to another (the email bot hands a meeting request
|
|
23
|
+
* to the calendar bot) with only the intent, the facts and the
|
|
24
|
+
* decision so far, never a whole conversation.
|
|
25
|
+
*
|
|
26
|
+
* Not every sentence is a fact. observe() decides what a statement does:
|
|
27
|
+
* - a joke, sarcasm, a hypothetical or someone else's words: ignored
|
|
28
|
+
* - something temporary: kept as live context with an expiry
|
|
29
|
+
* - a first mention in passing: tentative (shown as "not sure yet")
|
|
30
|
+
* - said plainly, or seen again: confirmed
|
|
31
|
+
* - contradicting a confirmed memory in passing: a challenger, which only
|
|
32
|
+
* replaces it after it is seen again, and then as tentative, marked for
|
|
33
|
+
* the user to review; said plainly ("I prefer pink now"), it replaces
|
|
34
|
+
* it and the old value stays in the history
|
|
35
|
+
* - anything the user set or corrected by hand is locked: the bots cannot
|
|
36
|
+
* change it, only flag the disagreement for review
|
|
37
|
+
* The user can see all of it, correct it, confirm it and remove it
|
|
38
|
+
* (Crew: Brain).
|
|
39
|
+
*
|
|
40
|
+
* Same SQLite drivers as session-db.js (node:sqlite, else better-sqlite3).
|
|
41
|
+
* With no driver (or CODEPLY_BRAIN=off) everything here is a quiet no-op.
|
|
42
|
+
*/
|
|
43
|
+
const fs = require('fs');
|
|
44
|
+
const os = require('os');
|
|
45
|
+
const path = require('path');
|
|
46
|
+
const crypto = require('crypto');
|
|
47
|
+
const { loadDriver } = require('./session-db.js');
|
|
48
|
+
|
|
49
|
+
const SCHEMA_VERSION = 1;
|
|
50
|
+
const HOUR = 3600000;
|
|
51
|
+
const DAY = 86400000;
|
|
52
|
+
const MAX_TEMP_HOURS = 24 * 14;
|
|
53
|
+
const MAX_HISTORY = 8;
|
|
54
|
+
const MAX_SOURCES = 8;
|
|
55
|
+
const CHALLENGE_TO_SWITCH = 2;
|
|
56
|
+
const MAX_HANDOFF_CHAIN = 3;
|
|
57
|
+
const BLOCK_LIMITS = { personal: 16, unsure: 4, live: 5, project: 12, lessons: 3, todos: 5 };
|
|
58
|
+
|
|
59
|
+
/** What can be remembered, and which layer it lives in. */
|
|
60
|
+
const KINDS = {
|
|
61
|
+
preference: { label: 'Preference', layer: 'personal' },
|
|
62
|
+
fact: { label: 'About them', layer: 'personal' },
|
|
63
|
+
routine: { label: 'Routine', layer: 'personal' },
|
|
64
|
+
style: { label: 'How they like to work', layer: 'personal' },
|
|
65
|
+
decision: { label: 'Decision', layer: 'project' },
|
|
66
|
+
idea: { label: 'Idea', layer: 'project' },
|
|
67
|
+
requirement: { label: 'Requirement', layer: 'project' },
|
|
68
|
+
todo: { label: 'To do', layer: 'project' },
|
|
69
|
+
context: { label: 'Right now', layer: 'live' },
|
|
70
|
+
lesson: { label: 'Lesson', layer: 'team' },
|
|
71
|
+
};
|
|
72
|
+
const KIND_KEYS = Object.keys(KINDS);
|
|
73
|
+
const PROJECT_KINDS = new Set(['decision', 'idea', 'requirement', 'todo']);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* How sure the speaker was. Only stated and implied ever become memory.
|
|
77
|
+
* stated said plainly about themselves ("my favorite color is red")
|
|
78
|
+
* implied follows from what they said or did, not said outright
|
|
79
|
+
* temporary true for now only ("I'm in Lahore this week")
|
|
80
|
+
* joking a joke, sarcasm, banter, said in an argument or to tease
|
|
81
|
+
* hypothetical "if I were...", "imagine...", a question, a maybe
|
|
82
|
+
* quoting someone else's words or preferences, a role play
|
|
83
|
+
*/
|
|
84
|
+
const CERTAINTY = ['stated', 'implied', 'temporary', 'joking', 'hypothetical', 'quoting'];
|
|
85
|
+
const IGNORED = new Set(['joking', 'hypothetical', 'quoting']);
|
|
86
|
+
|
|
87
|
+
const SECRET_RE = /(api[_ -]?key|password|passwd|passcode|secret|token|bearer|private key|pin code|cvv|card number|iban|ssn)\b|\bsk-[a-z0-9]{8,}/i;
|
|
88
|
+
|
|
89
|
+
const clip = (s, n) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, n);
|
|
90
|
+
const json = (v, fallback) => { try { return v == null ? fallback : JSON.parse(v); } catch { return fallback; } };
|
|
91
|
+
// "17:00", "5pm" or "2026-10-26T17:00" in the user's local time to epoch ms (0 if
|
|
92
|
+
// unreadable). A bare time that has already passed today means tomorrow.
|
|
93
|
+
function untilAt(s, t) {
|
|
94
|
+
const v = String(s || '').trim().toLowerCase();
|
|
95
|
+
if (!v) return 0;
|
|
96
|
+
let m = /^(\d{4})-(\d{2})-(\d{2})[t ](\d{1,2}):(\d{2})/.exec(v);
|
|
97
|
+
if (m) return new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]).getTime();
|
|
98
|
+
m = /^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/.exec(v);
|
|
99
|
+
if (!m || +m[1] > 23) return 0;
|
|
100
|
+
let h = +m[1];
|
|
101
|
+
if (m[3] === 'pm' && h < 12) h += 12;
|
|
102
|
+
if (m[3] === 'am' && h === 12) h = 0;
|
|
103
|
+
const d = new Date(t);
|
|
104
|
+
d.setHours(h, +(m[2] || 0), 0, 0);
|
|
105
|
+
return d.getTime() <= t ? d.getTime() + DAY : d.getTime();
|
|
106
|
+
}
|
|
107
|
+
/** "2026-10-26 17:05" in the user's local time. */
|
|
108
|
+
function localStamp(t) {
|
|
109
|
+
const d = new Date(t);
|
|
110
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
111
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
112
|
+
}
|
|
113
|
+
const norm = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
114
|
+
const newId = (p, now) => `${p}${now.toString(36)}${crypto.randomBytes(3).toString('hex')}`;
|
|
115
|
+
|
|
116
|
+
/** Same fact, or one that contains the other. */
|
|
117
|
+
function sameText(a, b) {
|
|
118
|
+
const x = norm(a); const y = norm(b);
|
|
119
|
+
if (!x || !y) return false;
|
|
120
|
+
if (x === y) return true;
|
|
121
|
+
const shorter = x.length < y.length ? x : y;
|
|
122
|
+
return shorter.length >= 12 && (x.includes(y) || y.includes(x));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const STOP = new Set('the and for with that this from what when where which your you our are was were can could would should will have has had not but all any how who why into onto about please just some them they then than there these those its it him her his she use using want need make get got give show tell check look find out new one two user users does did doing done like also very really'.split(' '));
|
|
126
|
+
/** "candidates", "candidate" and "candidating" all become "candidat"; "emails" becomes "email". */
|
|
127
|
+
function stem(w) {
|
|
128
|
+
if (w.length > 4 && w.endsWith('s') && !w.endsWith('ss')) w = w.slice(0, -1);
|
|
129
|
+
if (w.length > 5 && w.endsWith('ing')) w = w.slice(0, -3);
|
|
130
|
+
if (w.length > 4 && w.endsWith('e')) w = w.slice(0, -1);
|
|
131
|
+
return w;
|
|
132
|
+
}
|
|
133
|
+
/** Words that carry meaning, lightly stemmed ("emails" and "email" match). */
|
|
134
|
+
function terms(text) {
|
|
135
|
+
return [...new Set(norm(text).split(' ')
|
|
136
|
+
.filter((w) => w.length >= 3 && !STOP.has(w))
|
|
137
|
+
.map(stem))];
|
|
138
|
+
}
|
|
139
|
+
function overlap(a, b) {
|
|
140
|
+
const want = Array.isArray(a) ? a : terms(a);
|
|
141
|
+
const have = new Set(Array.isArray(b) ? b : terms(b));
|
|
142
|
+
return want.reduce((n, w) => n + (have.has(w) ? 1 : 0), 0);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** A project's key: its folder or name, lower case, dashes. */
|
|
146
|
+
function projectKey(nameOrPath) {
|
|
147
|
+
const s = String(nameOrPath || '').trim();
|
|
148
|
+
if (!s) return '';
|
|
149
|
+
const base = /[\\/]/.test(s) ? path.basename(s.replace(/[\\/]+$/, '')) : s;
|
|
150
|
+
return base.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── The store ──────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
// scratch: folders agents work in that are not projects (Crew's bots folder, the home folder).
|
|
156
|
+
let cfg = { file: null, now: null, modulePaths: null, scratch: [] };
|
|
157
|
+
let db = null; let q = null; let failed = false;
|
|
158
|
+
|
|
159
|
+
function defaultFile() {
|
|
160
|
+
return process.env.CODEPLY_BRAIN_DB || path.join(os.homedir(), '.codeply', 'brain', 'brain.db');
|
|
161
|
+
}
|
|
162
|
+
const now = () => (cfg.now ? cfg.now() : Date.now());
|
|
163
|
+
|
|
164
|
+
/** Hosts (Electron needs modulePaths for better-sqlite3) and tests point the brain somewhere. */
|
|
165
|
+
function configure(o = {}) {
|
|
166
|
+
close();
|
|
167
|
+
cfg = { ...cfg, ...o };
|
|
168
|
+
failed = false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function open() {
|
|
172
|
+
if (db) return true;
|
|
173
|
+
if (failed || process.env.CODEPLY_BRAIN === 'off') return false;
|
|
174
|
+
const driver = loadDriver(cfg.modulePaths || undefined);
|
|
175
|
+
if (!driver) { failed = true; return false; }
|
|
176
|
+
try {
|
|
177
|
+
const file = cfg.file || defaultFile();
|
|
178
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
179
|
+
db = driver.open(file);
|
|
180
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
181
|
+
db.exec('PRAGMA synchronous = NORMAL');
|
|
182
|
+
db.exec(`
|
|
183
|
+
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
184
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
185
|
+
id TEXT PRIMARY KEY, kind TEXT NOT NULL, scope TEXT NOT NULL, slot TEXT, value TEXT, text TEXT NOT NULL,
|
|
186
|
+
status TEXT NOT NULL, confidence REAL NOT NULL, evidence INTEGER NOT NULL, locked INTEGER NOT NULL DEFAULT 0,
|
|
187
|
+
review INTEGER NOT NULL DEFAULT 0, challenger TEXT, history TEXT NOT NULL DEFAULT '[]', sources TEXT NOT NULL DEFAULT '[]',
|
|
188
|
+
domain TEXT, expires_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, seen_at INTEGER NOT NULL
|
|
189
|
+
);
|
|
190
|
+
CREATE INDEX IF NOT EXISTS memories_scope ON memories (scope, kind);
|
|
191
|
+
CREATE INDEX IF NOT EXISTS memories_slot ON memories (scope, slot);
|
|
192
|
+
CREATE TABLE IF NOT EXISTS changes (
|
|
193
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT, at INTEGER NOT NULL, memory_id TEXT, action TEXT NOT NULL, text TEXT, source TEXT
|
|
194
|
+
);
|
|
195
|
+
CREATE INDEX IF NOT EXISTS changes_at ON changes (at);
|
|
196
|
+
CREATE TABLE IF NOT EXISTS projects (key TEXT PRIMARY KEY, name TEXT NOT NULL, path TEXT, updated_at INTEGER NOT NULL);
|
|
197
|
+
CREATE TABLE IF NOT EXISTS handoffs (
|
|
198
|
+
id TEXT PRIMARY KEY, from_bot TEXT, from_name TEXT, to_bot TEXT, to_name TEXT, domain TEXT,
|
|
199
|
+
intent TEXT NOT NULL, facts TEXT NOT NULL DEFAULT '[]', decision TEXT, requester TEXT NOT NULL,
|
|
200
|
+
origin TEXT, parent TEXT, task_id TEXT, status TEXT NOT NULL, result TEXT,
|
|
201
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
202
|
+
);
|
|
203
|
+
CREATE INDEX IF NOT EXISTS handoffs_status ON handoffs (status, created_at);
|
|
204
|
+
`);
|
|
205
|
+
db.prepare('INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)').run('schema', String(SCHEMA_VERSION));
|
|
206
|
+
q = {
|
|
207
|
+
get: db.prepare('SELECT * FROM memories WHERE id = ?'),
|
|
208
|
+
bySlot: db.prepare('SELECT * FROM memories WHERE scope = ? AND slot = ? AND status != ? ORDER BY updated_at DESC LIMIT 1'),
|
|
209
|
+
byScopeKind: db.prepare('SELECT * FROM memories WHERE scope = ? AND kind = ? ORDER BY updated_at DESC LIMIT 400'),
|
|
210
|
+
insert: db.prepare(`INSERT INTO memories (id, kind, scope, slot, value, text, status, confidence, evidence, locked, review, challenger, history, sources, domain, expires_at, created_at, updated_at, seen_at)
|
|
211
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
212
|
+
update: db.prepare(`UPDATE memories SET kind = ?, slot = ?, value = ?, text = ?, status = ?, confidence = ?, evidence = ?, locked = ?, review = ?,
|
|
213
|
+
challenger = ?, history = ?, sources = ?, domain = ?, expires_at = ?, updated_at = ?, seen_at = ? WHERE id = ?`),
|
|
214
|
+
remove: db.prepare('DELETE FROM memories WHERE id = ?'),
|
|
215
|
+
log: db.prepare('INSERT INTO changes (at, memory_id, action, text, source) VALUES (?, ?, ?, ?, ?)'),
|
|
216
|
+
changes: db.prepare('SELECT * FROM changes ORDER BY seq DESC LIMIT ?'),
|
|
217
|
+
changesFor: db.prepare('SELECT * FROM changes WHERE memory_id = ? ORDER BY seq DESC LIMIT 50'),
|
|
218
|
+
getMeta: db.prepare('SELECT value FROM meta WHERE key = ?'),
|
|
219
|
+
setMeta: db.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value'),
|
|
220
|
+
upsertProject: db.prepare(`INSERT INTO projects (key, name, path, updated_at) VALUES (?, ?, ?, ?)
|
|
221
|
+
ON CONFLICT(key) DO UPDATE SET name = excluded.name, path = COALESCE(excluded.path, projects.path), updated_at = excluded.updated_at`),
|
|
222
|
+
getProject: db.prepare('SELECT * FROM projects WHERE key = ?'),
|
|
223
|
+
projects: db.prepare('SELECT * FROM projects ORDER BY updated_at DESC LIMIT ?'),
|
|
224
|
+
insertHandoff: db.prepare(`INSERT INTO handoffs (id, from_bot, from_name, to_bot, to_name, domain, intent, facts, decision, requester, origin, parent, task_id, status, result, created_at, updated_at)
|
|
225
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
226
|
+
getHandoff: db.prepare('SELECT * FROM handoffs WHERE id = ?'),
|
|
227
|
+
updateHandoff: db.prepare('UPDATE handoffs SET to_bot = ?, to_name = ?, task_id = ?, status = ?, result = ?, updated_at = ? WHERE id = ?'),
|
|
228
|
+
handoffsByStatus: db.prepare('SELECT * FROM handoffs WHERE status = ? ORDER BY created_at ASC LIMIT ?'),
|
|
229
|
+
recentHandoffs: db.prepare('SELECT * FROM handoffs ORDER BY updated_at DESC LIMIT ?'),
|
|
230
|
+
handoffByTask: db.prepare('SELECT * FROM handoffs WHERE task_id = ? LIMIT 1'),
|
|
231
|
+
};
|
|
232
|
+
return true;
|
|
233
|
+
} catch (e) {
|
|
234
|
+
failed = true;
|
|
235
|
+
try { if (db) db.close(); } catch {}
|
|
236
|
+
db = null; q = null;
|
|
237
|
+
if (process.env.CODEPLY_DEBUG) console.warn('[brain]', e.message);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function close() {
|
|
243
|
+
try { if (db) db.close(); } catch {}
|
|
244
|
+
db = null; q = null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** This machine can keep a brain at all (a SQLite driver loads). */
|
|
248
|
+
function usable() { return open(); }
|
|
249
|
+
/** The brain is usable here and the user has not switched it off. */
|
|
250
|
+
function available() { return open() && enabled(); }
|
|
251
|
+
function enabled() {
|
|
252
|
+
if (!open()) return false;
|
|
253
|
+
const r = q.getMeta.get('enabled');
|
|
254
|
+
return !r || r.value !== '0';
|
|
255
|
+
}
|
|
256
|
+
function setEnabled(on) {
|
|
257
|
+
if (!open()) return false;
|
|
258
|
+
q.setMeta.run('enabled', on ? '1' : '0');
|
|
259
|
+
return !!on;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ─── Memories ───────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
function rowToMemory(r) {
|
|
265
|
+
if (!r) return null;
|
|
266
|
+
return {
|
|
267
|
+
id: r.id, kind: r.kind, scope: r.scope, slot: r.slot || '', value: r.value || '', text: r.text,
|
|
268
|
+
status: r.status, confidence: Math.round(Number(r.confidence) * 100) / 100, evidence: Number(r.evidence) || 1,
|
|
269
|
+
locked: !!r.locked, review: !!r.review, challenger: json(r.challenger, null), history: json(r.history, []),
|
|
270
|
+
sources: json(r.sources, []), domain: r.domain || '', expiresAt: r.expires_at == null ? null : Number(r.expires_at),
|
|
271
|
+
createdAt: Number(r.created_at), updatedAt: Number(r.updated_at), seenAt: Number(r.seen_at),
|
|
272
|
+
project: String(r.scope).startsWith('project:') ? r.scope.slice(8) : '',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Where a memory came from: { product, botId, botName }. Product defaults to the running one (telemetry.js). */
|
|
277
|
+
function sourceOf(src) {
|
|
278
|
+
const s = src && typeof src === 'object' ? src : {};
|
|
279
|
+
let product = s.product;
|
|
280
|
+
if (!product) { try { product = require('./telemetry.js').currentProduct(); } catch {} }
|
|
281
|
+
return { product: clip(product || 'craft', 20), botId: s.botId ? clip(s.botId, 60) : '', botName: s.botName ? clip(s.botName, 40) : '', at: now() };
|
|
282
|
+
}
|
|
283
|
+
const sourceLabel = (s) => (s ? [s.botName, s.product].filter(Boolean).join(' in ') : '');
|
|
284
|
+
function addSource(list, src) {
|
|
285
|
+
const out = (Array.isArray(list) ? list : []).filter((x) => !(x.product === src.product && x.botId === src.botId));
|
|
286
|
+
out.push(src);
|
|
287
|
+
return out.slice(-MAX_SOURCES);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function write(m) {
|
|
291
|
+
q.update.run(m.kind, m.slot || null, m.value || null, m.text, m.status, m.confidence, m.evidence, m.locked ? 1 : 0, m.review ? 1 : 0,
|
|
292
|
+
m.challenger ? JSON.stringify(m.challenger) : null, JSON.stringify((m.history || []).slice(-MAX_HISTORY)), JSON.stringify(m.sources || []),
|
|
293
|
+
m.domain || null, m.expiresAt == null ? null : Math.round(m.expiresAt), now(), m.seenAt || now(), m.id);
|
|
294
|
+
return get(m.id);
|
|
295
|
+
}
|
|
296
|
+
function log(memoryId, action, text, src) {
|
|
297
|
+
try { q.log.run(now(), memoryId || null, action, clip(text, 300), src ? sourceLabel(src) : ''); } catch {}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** The scope an observation belongs to. */
|
|
301
|
+
function scopeFor(o, kind) {
|
|
302
|
+
if (kind === 'lesson') return 'team';
|
|
303
|
+
if (o.scope && /^(me|team|project:[a-z0-9-]+)$/.test(o.scope)) return o.scope;
|
|
304
|
+
const pk = knownKey(o.project);
|
|
305
|
+
if (pk) return `project:${pk}`;
|
|
306
|
+
return 'me';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Turn whatever a model or a tool sent into a clean observation, or null. */
|
|
310
|
+
function cleanObservation(o) {
|
|
311
|
+
if (!o || typeof o !== 'object') return null;
|
|
312
|
+
let kind = String(o.kind || '').toLowerCase().trim();
|
|
313
|
+
if (kind === 'temporary' || kind === 'live' || kind === 'now') kind = 'context';
|
|
314
|
+
if (kind === 'preferences') kind = 'preference';
|
|
315
|
+
if (!KINDS[kind]) kind = o.project ? 'decision' : 'fact';
|
|
316
|
+
let certainty = String(o.certainty || o.how || '').toLowerCase().trim();
|
|
317
|
+
if (certainty === 'sarcastic' || certainty === 'sarcasm' || certainty === 'joke' || certainty === 'teasing') certainty = 'joking';
|
|
318
|
+
if (certainty === 'explicit' || certainty === 'direct' || certainty === 'said') certainty = 'stated';
|
|
319
|
+
if (certainty === 'question' || certainty === 'maybe' || certainty === 'wish') certainty = 'hypothetical';
|
|
320
|
+
if (!CERTAINTY.includes(certainty)) certainty = 'implied';
|
|
321
|
+
if (kind === 'context') certainty = IGNORED.has(certainty) ? certainty : 'temporary';
|
|
322
|
+
if (certainty === 'temporary' && kind !== 'context') kind = 'context';
|
|
323
|
+
const value = clip(o.value, 200);
|
|
324
|
+
const slot = norm(o.slot).slice(0, 80);
|
|
325
|
+
const text = clip(o.text || o.fact || (slot && value ? `${slot}: ${value}` : value), 280);
|
|
326
|
+
if (!text || text.length < 3) return null;
|
|
327
|
+
return {
|
|
328
|
+
kind, certainty, slot, value, text,
|
|
329
|
+
project: clip(o.project, 80), scope: o.scope ? String(o.scope) : '',
|
|
330
|
+
ttlHours: Number(o.ttlHours || o.ttl_hours) || 0, until: untilAt(o.until || o.expires, now()), domain: clip(o.domain, 40).toLowerCase(),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function findExisting(scope, o) {
|
|
335
|
+
if (o.slot) {
|
|
336
|
+
const r = q.bySlot.get(scope, o.slot, '__none__');
|
|
337
|
+
if (r) return rowToMemory(r);
|
|
338
|
+
}
|
|
339
|
+
for (const r of q.byScopeKind.all(scope, o.kind)) {
|
|
340
|
+
const m = rowToMemory(r);
|
|
341
|
+
if (m.status === 'temporary' && m.expiresAt != null && m.expiresAt < now()) continue;
|
|
342
|
+
if (sameText(m.text, o.text)) return m;
|
|
343
|
+
}
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const sameValue = (m, o) => (o.value && m.value ? norm(o.value) === norm(m.value) : sameText(m.text, o.text));
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* One thing Codeply heard or was told. Decides what it does to memory.
|
|
351
|
+
* @param {object} raw { kind, text, slot?, value?, certainty?, project?, scope?, ttlHours?, domain? }
|
|
352
|
+
* @param {object} [src] { product, botId, botName }
|
|
353
|
+
* @returns {{ action: 'added'|'reinforced'|'replaced'|'challenged'|'kept'|'ignored', memory?: object, why?: string }}
|
|
354
|
+
*/
|
|
355
|
+
function observe(raw, src) {
|
|
356
|
+
if (!available()) return { action: 'ignored', why: 'the brain is off' };
|
|
357
|
+
const o = cleanObservation(raw);
|
|
358
|
+
if (!o) return { action: 'ignored', why: 'nothing to keep' };
|
|
359
|
+
const from = sourceOf(src);
|
|
360
|
+
if (SECRET_RE.test(`${o.text} ${o.value} ${o.slot}`)) { log(null, 'ignored', 'something that looked like a secret', from); return { action: 'ignored', why: 'looks like a secret' }; }
|
|
361
|
+
if (IGNORED.has(o.certainty)) {
|
|
362
|
+
log(null, 'ignored', `${o.text} (${o.certainty === 'joking' ? 'a joke or sarcasm' : o.certainty === 'quoting' ? 'someone else\'s words' : 'hypothetical'})`, from);
|
|
363
|
+
return { action: 'ignored', why: o.certainty };
|
|
364
|
+
}
|
|
365
|
+
const scope = scopeFor(o, o.kind);
|
|
366
|
+
if (scope.startsWith('project:')) touchProject({ name: o.project || scope.slice(8) });
|
|
367
|
+
const t = now();
|
|
368
|
+
|
|
369
|
+
// Live context: expires by itself, never touches what is known for good.
|
|
370
|
+
if (o.kind === 'context') {
|
|
371
|
+
// "back around 17:00" ends at 17:00: an end time the model read from the user beats a guessed number of hours.
|
|
372
|
+
const end = o.until > t && o.until <= t + MAX_TEMP_HOURS * HOUR ? o.until : t + Math.max(1, Math.min(MAX_TEMP_HOURS, o.ttlHours || 24)) * HOUR;
|
|
373
|
+
const cur = findExisting(scope, { ...o, slot: o.slot ? `now ${o.slot}` : '' });
|
|
374
|
+
if (cur && cur.kind === 'context') {
|
|
375
|
+
const m = write({ ...cur, text: o.text, value: o.value, expiresAt: end, evidence: cur.evidence + 1, sources: addSource(cur.sources, from), seenAt: t });
|
|
376
|
+
return { action: 'reinforced', memory: m };
|
|
377
|
+
}
|
|
378
|
+
const id = newId('m', t);
|
|
379
|
+
q.insert.run(id, 'context', scope, o.slot ? `now ${o.slot}` : null, o.value || null, o.text, 'temporary', 0.7, 1, 0, 0, null, '[]', JSON.stringify([from]), o.domain || null, end, t, t, t);
|
|
380
|
+
log(id, 'added', `${o.text} (for now)`, from);
|
|
381
|
+
return { action: 'added', memory: get(id) };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const stated = o.certainty === 'stated';
|
|
385
|
+
const cur = findExisting(scope, o);
|
|
386
|
+
if (!cur) {
|
|
387
|
+
const id = newId('m', t);
|
|
388
|
+
q.insert.run(id, o.kind, scope, o.slot || null, o.value || null, o.text, stated ? 'confirmed' : 'tentative', stated ? 0.8 : 0.5, 1, 0, 0, null, '[]', JSON.stringify([from]), o.domain || null, null, t, t, t);
|
|
389
|
+
log(id, 'added', `${o.text}${stated ? '' : ' (not sure yet)'}`, from);
|
|
390
|
+
return { action: 'added', memory: get(id) };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (sameValue(cur, o)) {
|
|
394
|
+
const evidence = cur.evidence + 1;
|
|
395
|
+
const confirm = cur.status === 'tentative' && (stated || evidence >= 2);
|
|
396
|
+
const m = write({
|
|
397
|
+
...cur, text: cur.locked ? cur.text : o.text, evidence, confidence: Math.min(1, cur.confidence + (stated ? 0.2 : 0.1)),
|
|
398
|
+
status: confirm ? 'confirmed' : cur.status, challenger: null, sources: addSource(cur.sources, from), seenAt: t,
|
|
399
|
+
});
|
|
400
|
+
log(cur.id, confirm ? 'confirmed' : 'reinforced', m.text, from);
|
|
401
|
+
return { action: 'reinforced', memory: m };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// A different value for the same thing.
|
|
405
|
+
const entry = { value: cur.value, text: cur.text, status: cur.status, until: t };
|
|
406
|
+
if (cur.locked) {
|
|
407
|
+
const m = write({ ...cur, challenger: { value: o.value, text: o.text, count: ((cur.challenger && sameText(cur.challenger.text, o.text)) ? cur.challenger.count : 0) + 1, at: t, from: sourceLabel(from) }, review: stated || cur.review, seenAt: t });
|
|
408
|
+
log(cur.id, 'kept', `kept "${cur.text}" (you set it), heard "${o.text}"`, from);
|
|
409
|
+
return { action: 'kept', memory: m, why: 'you set this one yourself' };
|
|
410
|
+
}
|
|
411
|
+
if (stated || cur.status === 'tentative') {
|
|
412
|
+
const m = write({
|
|
413
|
+
...cur, kind: o.kind, slot: o.slot || cur.slot, value: o.value, text: o.text, status: stated ? 'confirmed' : 'tentative',
|
|
414
|
+
confidence: stated ? 0.8 : 0.5, evidence: 1, challenger: null, review: false, history: [...cur.history, entry], sources: addSource(cur.sources, from), seenAt: t,
|
|
415
|
+
});
|
|
416
|
+
log(cur.id, 'replaced', `"${cur.text}" -> "${o.text}"`, from);
|
|
417
|
+
return { action: 'replaced', memory: m };
|
|
418
|
+
}
|
|
419
|
+
// Confirmed, and contradicted in passing: one remark is not enough.
|
|
420
|
+
const ch = cur.challenger && (o.value ? norm(cur.challenger.value) === norm(o.value) : sameText(cur.challenger.text, o.text)) ? cur.challenger : null;
|
|
421
|
+
const count = (ch ? ch.count : 0) + 1;
|
|
422
|
+
if (count >= CHALLENGE_TO_SWITCH) {
|
|
423
|
+
const m = write({
|
|
424
|
+
...cur, value: o.value, text: o.text, status: 'tentative', confidence: 0.55, evidence: count, challenger: null, review: true,
|
|
425
|
+
history: [...cur.history, entry], sources: addSource(cur.sources, from), seenAt: t,
|
|
426
|
+
});
|
|
427
|
+
log(cur.id, 'replaced', `"${cur.text}" -> "${o.text}" (heard ${count} times; check it)`, from);
|
|
428
|
+
return { action: 'replaced', memory: m };
|
|
429
|
+
}
|
|
430
|
+
const m = write({ ...cur, challenger: { value: o.value, text: o.text, count, at: t, from: sourceLabel(from) }, seenAt: t });
|
|
431
|
+
log(cur.id, 'challenged', `still "${cur.text}"; heard "${o.text}" once`, from);
|
|
432
|
+
return { action: 'challenged', memory: m };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function get(id) { return open() ? rowToMemory(q.get.get(String(id))) : null; }
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Memories, for the panel and the prompt.
|
|
439
|
+
* @param {{ scope?: string, scopes?: string[], kinds?: string[], status?: string, text?: string, includeExpired?: boolean, limit?: number }} [f]
|
|
440
|
+
*/
|
|
441
|
+
function list(f = {}) {
|
|
442
|
+
if (!open()) return [];
|
|
443
|
+
const where = []; const args = [];
|
|
444
|
+
const scopes = f.scopes || (f.scope ? [f.scope] : null);
|
|
445
|
+
if (scopes && scopes.length) { where.push(`scope IN (${scopes.map(() => '?').join(',')})`); args.push(...scopes); }
|
|
446
|
+
if (f.kinds && f.kinds.length) { where.push(`kind IN (${f.kinds.map(() => '?').join(',')})`); args.push(...f.kinds); }
|
|
447
|
+
if (f.status) { where.push('status = ?'); args.push(f.status); }
|
|
448
|
+
if (!f.includeExpired) { where.push('(expires_at IS NULL OR expires_at >= ?)'); args.push(now()); }
|
|
449
|
+
if (f.text) {
|
|
450
|
+
const like = `%${String(f.text).replace(/[\\%_]/g, (c) => `\\${c}`)}%`;
|
|
451
|
+
where.push("(text LIKE ? ESCAPE '\\' OR slot LIKE ? ESCAPE '\\' OR value LIKE ? ESCAPE '\\')"); args.push(like, like, like);
|
|
452
|
+
}
|
|
453
|
+
args.push(Math.max(1, Math.min(1000, Number(f.limit) || 300)));
|
|
454
|
+
return db.prepare(`SELECT * FROM memories${where.length ? ` WHERE ${where.join(' AND ')}` : ''} ORDER BY updated_at DESC LIMIT ?`).all(...args).map(rowToMemory);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** The user fixes a memory by hand: it is locked, so no bot can change it again. */
|
|
458
|
+
function correct(id, patch = {}) {
|
|
459
|
+
const cur = get(id);
|
|
460
|
+
if (!cur) throw new Error('That memory is gone.');
|
|
461
|
+
const text = clip(patch.text != null ? patch.text : cur.text, 280);
|
|
462
|
+
if (!text) throw new Error('A memory needs some words.');
|
|
463
|
+
if (SECRET_RE.test(text)) throw new Error('That looks like a secret. Codeply does not keep passwords or keys.');
|
|
464
|
+
const changed = text !== cur.text || (patch.value != null && clip(patch.value, 200) !== cur.value);
|
|
465
|
+
const m = write({
|
|
466
|
+
...cur, text, value: patch.value != null ? clip(patch.value, 200) : cur.value,
|
|
467
|
+
kind: KINDS[patch.kind] ? patch.kind : cur.kind, status: cur.kind === 'context' ? 'temporary' : 'confirmed',
|
|
468
|
+
confidence: 1, locked: true, review: false, challenger: null,
|
|
469
|
+
history: changed ? [...cur.history, { value: cur.value, text: cur.text, status: cur.status, until: now() }] : cur.history,
|
|
470
|
+
});
|
|
471
|
+
log(id, 'corrected', changed ? `"${cur.text}" -> "${text}"` : `confirmed "${text}"`, { product: 'you' });
|
|
472
|
+
return m;
|
|
473
|
+
}
|
|
474
|
+
/** "Yes, that's right": confirmed and locked as it is. */
|
|
475
|
+
const confirm = (id) => correct(id, {});
|
|
476
|
+
|
|
477
|
+
/** A flagged disagreement: keep what is there, or switch to what was heard. */
|
|
478
|
+
function resolve(id, choice) {
|
|
479
|
+
const cur = get(id);
|
|
480
|
+
if (!cur) throw new Error('That memory is gone.');
|
|
481
|
+
if (choice === 'switch' && cur.challenger) {
|
|
482
|
+
return correct(id, { text: cur.challenger.text, value: cur.challenger.value || '' });
|
|
483
|
+
}
|
|
484
|
+
if (choice === 'undo' && cur.history.length) {
|
|
485
|
+
const prev = cur.history[cur.history.length - 1];
|
|
486
|
+
const m = correct(id, { text: prev.text, value: prev.value || '' });
|
|
487
|
+
return write({ ...m, history: m.history.slice(0, -2) });
|
|
488
|
+
}
|
|
489
|
+
const m = write({ ...cur, challenger: null, review: false });
|
|
490
|
+
log(id, 'kept', `kept "${cur.text}"`, { product: 'you' });
|
|
491
|
+
return m;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function forget(id) {
|
|
495
|
+
const cur = get(id);
|
|
496
|
+
if (!cur) return false;
|
|
497
|
+
q.remove.run(cur.id);
|
|
498
|
+
log(cur.id, 'forgotten', cur.text, { product: 'you' });
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Forget a whole scope ("me", "team", "project:x"), or everything. */
|
|
503
|
+
function clear(scope) {
|
|
504
|
+
if (!open()) return 0;
|
|
505
|
+
const n = scope ? db.prepare('DELETE FROM memories WHERE scope = ?').run(String(scope)).changes : db.prepare('DELETE FROM memories').run().changes;
|
|
506
|
+
log(null, 'forgotten', scope ? `everything in ${scope}` : 'everything', { product: 'you' });
|
|
507
|
+
return Number(n) || 0;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const rowToChange = (r) => r && { seq: Number(r.seq), at: Number(r.at), memoryId: r.memory_id || '', action: r.action, text: r.text || '', source: r.source || '' };
|
|
511
|
+
/** What changed, newest first (all, or one memory's story). */
|
|
512
|
+
function changes(o = {}) {
|
|
513
|
+
if (!open()) return [];
|
|
514
|
+
return (o.id ? q.changesFor.all(String(o.id)) : q.changes.all(Math.max(1, Math.min(500, Number(o.limit) || 60)))).map(rowToChange);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Expired live context goes; so do changes older than a year. */
|
|
518
|
+
function prune() {
|
|
519
|
+
if (!open()) return;
|
|
520
|
+
db.prepare('DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?').run(now() - DAY);
|
|
521
|
+
db.prepare('DELETE FROM changes WHERE at < ?').run(now() - 365 * DAY);
|
|
522
|
+
db.prepare("DELETE FROM handoffs WHERE status IN ('done', 'failed', 'cancelled') AND updated_at < ?").run(now() - 90 * DAY);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ─── Projects ───────────────────────────────────────────────────────────────
|
|
526
|
+
|
|
527
|
+
const rowToProject = (r) => r && { key: r.key, name: r.name, path: r.path || '', updatedAt: Number(r.updated_at) };
|
|
528
|
+
/** A project the brain should know by name (Craft calls this for every folder it runs in). */
|
|
529
|
+
function touchProject({ name, path: p } = {}) {
|
|
530
|
+
if (!open()) return null;
|
|
531
|
+
const key = knownKey(name || p);
|
|
532
|
+
if (!key || key.length < 2) return null;
|
|
533
|
+
const cur = rowToProject(q.getProject.get(key));
|
|
534
|
+
q.upsertProject.run(key, clip(name || (cur && cur.name) || path.basename(String(p || key)), 80), p ? String(p).slice(0, 500) : null, now());
|
|
535
|
+
return rowToProject(q.getProject.get(key));
|
|
536
|
+
}
|
|
537
|
+
function projects(limit = 30) { return open() ? q.projects.all(limit).map(rowToProject) : []; }
|
|
538
|
+
|
|
539
|
+
/** The project a working folder is, or '' for a scratch folder (Crew's bots folder, home). */
|
|
540
|
+
function projectOfFolder(cwd) {
|
|
541
|
+
const p = String(cwd || '').trim();
|
|
542
|
+
if (!p) return '';
|
|
543
|
+
const full = path.resolve(p);
|
|
544
|
+
const scratch = [os.homedir(), path.join(os.homedir(), 'Codeply Crew'), ...(cfg.scratch || [])].map((x) => path.resolve(String(x)));
|
|
545
|
+
if (scratch.includes(full) || full === path.parse(full).root) return '';
|
|
546
|
+
return path.basename(full);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* The key of a project the brain already knows by this name or folder, else
|
|
551
|
+
* the plain key. A folder called study-buddy is the "HackLahore study buddy"
|
|
552
|
+
* the user talked about in Crew: one key's words run whole inside the other's.
|
|
553
|
+
* With several matches, the most recently used wins.
|
|
554
|
+
*/
|
|
555
|
+
function knownKey(nameOrPath) {
|
|
556
|
+
const key = projectKey(nameOrPath);
|
|
557
|
+
if (!key || !open() || q.getProject.get(key)) return key;
|
|
558
|
+
const inside = (a, b) => `-${b}-`.includes(`-${a}-`);
|
|
559
|
+
const specific = (k) => k.split('-').length >= 2 || k.length >= 8;
|
|
560
|
+
const hit = projects(60).find((p) => (specific(key) && inside(key, p.key)) || (specific(p.key) && inside(p.key, key)));
|
|
561
|
+
return hit ? hit.key : key;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** The project a folder or name means, matching known projects by key. */
|
|
565
|
+
function projectFor(nameOrPath) {
|
|
566
|
+
if (!open()) return null;
|
|
567
|
+
const key = knownKey(nameOrPath);
|
|
568
|
+
if (!key) return null;
|
|
569
|
+
return rowToProject(q.getProject.get(key));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** Known projects a message names ("the codeply craft app" names codeply-craft). */
|
|
573
|
+
function projectsNamed(text) {
|
|
574
|
+
const hay = ` ${norm(text).replace(/ /g, '-')} `.replace(/-/g, ' ');
|
|
575
|
+
return projects(60).filter((p) => {
|
|
576
|
+
const words = p.key.split('-').filter(Boolean);
|
|
577
|
+
if (!words.length || p.key.length < 3) return false;
|
|
578
|
+
return hay.includes(` ${words.join(' ')} `);
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ─── What goes in every prompt ──────────────────────────────────────────────
|
|
583
|
+
|
|
584
|
+
const ago = (ms) => { const m = Math.round(ms / 60000); if (m < 60) return `${Math.max(1, m)} min`; const h = Math.round(m / 60); if (h < 48) return `${h} h`; return `${Math.round(h / 24)} days`; };
|
|
585
|
+
|
|
586
|
+
function rank(list, want, n) {
|
|
587
|
+
return list.map((m) => ({ m, s: (want.length ? overlap(want, `${m.slot} ${m.value} ${m.text}`) * 2 : 0) + m.confidence + Math.min(3, m.evidence) * 0.3 + (m.locked ? 1 : 0) + Math.max(0, 1 - (now() - m.seenAt) / (60 * DAY)) }))
|
|
588
|
+
.sort((a, b) => b.s - a.s).slice(0, n).map((x) => x.m);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Lessons teammates learned that fit this request (never the bot's own). */
|
|
592
|
+
function teamLessons({ excludeBotId, request, domains, limit = BLOCK_LIMITS.lessons } = {}) {
|
|
593
|
+
if (!available()) return [];
|
|
594
|
+
const want = terms(request);
|
|
595
|
+
const doms = new Set((domains || []).map((d) => String(d).toLowerCase()));
|
|
596
|
+
return list({ scope: 'team', kinds: ['lesson'], limit: 200 })
|
|
597
|
+
.filter((m) => !excludeBotId || !m.sources.every((s) => s.botId === excludeBotId))
|
|
598
|
+
.map((m) => ({ m, s: overlap(want, m.text) + (m.domain && doms.has(m.domain) ? 1.5 : 0) + (m.status === 'confirmed' ? 0.5 : 0) }))
|
|
599
|
+
.filter((x) => x.s >= 2)
|
|
600
|
+
.sort((a, b) => b.s - a.s).slice(0, limit).map((x) => x.m);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const REMEMBER_FORMAT = `To save something, write:
|
|
604
|
+
<codeply:remember>
|
|
605
|
+
<kind>preference</kind>
|
|
606
|
+
<slot>reply length</slot>
|
|
607
|
+
<value>short</value>
|
|
608
|
+
<text>The user wants short replies.</text>
|
|
609
|
+
<certainty>stated</certainty>
|
|
610
|
+
</codeply:remember>
|
|
611
|
+
(kind: preference, fact, routine, style, decision, idea, requirement, todo, context; add <project>name</project> for project things; for context, <until>17:00</until> (their local time) says when it stops being true; <forget>id</forget> removes one.)`;
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* The brain block for one run's prompt. Empty when there is nothing to say.
|
|
615
|
+
* @param {{ cwd?: string, project?: string, request?: string, botId?: string, native?: boolean, domains?: string[] }} [o]
|
|
616
|
+
*/
|
|
617
|
+
function contextBlock(o = {}) {
|
|
618
|
+
if (!available()) return '';
|
|
619
|
+
try {
|
|
620
|
+
const want = terms(o.request);
|
|
621
|
+
const keys = new Set();
|
|
622
|
+
const here = knownKey(o.project || projectOfFolder(o.cwd));
|
|
623
|
+
if (here && projectFor(here)) keys.add(here);
|
|
624
|
+
for (const p of projectsNamed(o.request || '')) keys.add(p.key);
|
|
625
|
+
// Standing rules count as personal too: "always CC hiring@" may come back as a requirement or decision.
|
|
626
|
+
const personal = list({ scope: 'me', kinds: ['preference', 'fact', 'routine', 'style', 'decision', 'requirement'], limit: 300 });
|
|
627
|
+
const sure = rank(personal.filter((m) => m.status === 'confirmed'), want, BLOCK_LIMITS.personal);
|
|
628
|
+
const unsure = rank(personal.filter((m) => m.status === 'tentative'), want, 20).filter((m) => !want.length || overlap(want, m.text) > 0 || m.evidence > 1).slice(0, BLOCK_LIMITS.unsure);
|
|
629
|
+
const live = list({ kinds: ['context'], limit: 20 }).slice(0, BLOCK_LIMITS.live);
|
|
630
|
+
const lines = [];
|
|
631
|
+
const line = (m) => `- ${m.text}${m.review ? ' (recently changed; double check)' : ''}`;
|
|
632
|
+
if (sure.length) lines.push('About them:', ...sure.map(line));
|
|
633
|
+
if (unsure.length) lines.push('Not sure yet (heard once or in passing; never act on it without checking):', ...unsure.map((m) => `- ${m.text}`));
|
|
634
|
+
// Open to-dos from the last three weeks always; ideas only when this message is about them.
|
|
635
|
+
const todos = list({ scope: 'me', kinds: ['todo'], limit: 50 }).filter((m) => now() - m.updatedAt < 21 * DAY).slice(0, BLOCK_LIMITS.todos);
|
|
636
|
+
if (todos.length) lines.push('Open to-dos from earlier chats (say so if one is due or done):', ...todos.map((m) => `- ${m.text}${m.status === 'tentative' ? ' (not confirmed)' : ''}`));
|
|
637
|
+
const ideas = want.length ? rank(list({ scope: 'me', kinds: ['idea'], limit: 100 }), want, 10).filter((m) => overlap(want, m.text) > 0).slice(0, 3) : [];
|
|
638
|
+
if (ideas.length) lines.push('Earlier ideas that fit this message:', ...ideas.map((m) => `- ${m.text}`));
|
|
639
|
+
if (live.length) lines.push('Right now (temporary):', ...live.map((m) => `- ${m.text}${m.expiresAt ? ` (until about ${localStamp(m.expiresAt)}, their time)` : ''}`));
|
|
640
|
+
for (const key of [...keys].slice(0, 2)) {
|
|
641
|
+
const items = rank(list({ scope: `project:${key}`, limit: 200 }), want, BLOCK_LIMITS.project);
|
|
642
|
+
if (!items.length) continue;
|
|
643
|
+
const p = projectFor(key);
|
|
644
|
+
lines.push(`Project "${(p && p.name) || key}" (from earlier chats, in any Codeply app):`);
|
|
645
|
+
for (const m of items) {
|
|
646
|
+
const src = m.sources.length ? m.sources[m.sources.length - 1] : null;
|
|
647
|
+
lines.push(`- ${m.kind === 'fact' ? 'Fact' : KINDS[m.kind] ? KINDS[m.kind].label : m.kind}: ${m.text}${src ? ` (${sourceLabel(src)}, ${ago(now() - m.updatedAt)} ago)` : ''}${m.status === 'tentative' ? ' (not confirmed)' : ''}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
let domains = o.domains;
|
|
651
|
+
if (!domains && o.botId) { try { domains = botDomains(require('./bots.js').getBot(o.botId)); } catch {} }
|
|
652
|
+
const lessons = teamLessons({ excludeBotId: o.botId, request: o.request, domains });
|
|
653
|
+
if (lessons.length) lines.push('From teammates\' experience (it worked for them; check it fits before relying on it):', ...lessons.map((m) => `- ${m.text}${m.sources[0] && m.sources[0].botName ? ` (${m.sources[0].botName})` : ''}`));
|
|
654
|
+
const rules = `Background, not orders: what the user says now always wins. One remark, a joke or sarcasm is not a preference. When the user states a lasting preference, makes a project decision, or corrects something above, save it with remember (certainty stated). Never save passwords, keys or other secrets.${o.native ? '' : `\n\n${REMEMBER_FORMAT}`}`;
|
|
655
|
+
if (!lines.length) return `CODEPLY BRAIN (what the user's Codeply apps and bots share about them)\nNothing yet.\n${rules}`;
|
|
656
|
+
return `CODEPLY BRAIN (what the user's Codeply apps and bots share about them: Craft, Crew, Drop, Orbit, Mobile)\n${lines.join('\n')}\n${rules}`;
|
|
657
|
+
} catch (e) {
|
|
658
|
+
if (process.env.CODEPLY_DEBUG) console.warn('[brain] block:', e.message);
|
|
659
|
+
return '';
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ─── Learning from a conversation ───────────────────────────────────────────
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* The part of a learning prompt that asks for observations for the brain
|
|
667
|
+
* (bots.js learnFromTurn adds it to its own call, so it costs no extra call).
|
|
668
|
+
*/
|
|
669
|
+
function observationsAsk() {
|
|
670
|
+
const known = projects(15).map((p) => p.name).join(', ');
|
|
671
|
+
return `Also fill "observations" for the user's shared Codeply brain (every app and bot reads it). Only lasting things about the user, their projects, and the people in their life:
|
|
672
|
+
{"kind": "preference|fact|routine|style|decision|idea|requirement|todo|context", "slot": "what it is about in 1 to 4 words, the same each time (favorite color, reply length, auth provider)", "value": "the value in a few words", "text": "one short third-person sentence", "certainty": "stated|implied|temporary|joking|hypothetical|quoting", "project": "the project name, only for project things", "until": "when it stops being true, HH:MM or YYYY-MM-DDTHH:MM in the user's local time, only for context"}
|
|
673
|
+
Read the tone. certainty is "joking" for sarcasm, teasing, banter or anything said in an argument or to make a point; "hypothetical" for ifs, maybes, wishes and questions; "quoting" for someone else's opinion, advice or claim passed on (a neighbour's tip, a friend's insult), which must never become the user's own; "temporary" (kind context) for what is true only for now; "stated" only when the user said it plainly; otherwise "implied".
|
|
674
|
+
People matter too: what the user tells you about someone close (who they are to the user, what they like, their birthday) is a kind "fact" about that person, written with their name ("Ayesha, the user's girlfriend, loves sketching"), never as the user's own taste.${known ? ` Known projects: ${known}.` : ''} Most exchanges have none: use an empty list. At most 4.`;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** Store what a learning call returned. Returns the outcome of each. */
|
|
678
|
+
function noteObservations(list, src) {
|
|
679
|
+
if (!available() || !Array.isArray(list)) return [];
|
|
680
|
+
return list.slice(0, 6).map((o) => ({ text: o && o.text, ...observe(o, src) }));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* A learning call of its own (for chats with no bot memory call to ride on).
|
|
685
|
+
* chatFn has the ai.js chatJson shape. Never throws.
|
|
686
|
+
*/
|
|
687
|
+
async function learnFromExchange({ userText, replyText, project, src }, chatFn) {
|
|
688
|
+
try {
|
|
689
|
+
if (!available() || typeof chatFn !== 'function') return [];
|
|
690
|
+
const user = clip(userText, 3000);
|
|
691
|
+
if (user.length < 12) return [];
|
|
692
|
+
const prompt = `You keep the shared memory of a user's Codeply apps.${project ? ` This chat is about the project "${project}".` : ''}
|
|
693
|
+
|
|
694
|
+
User: ${user}
|
|
695
|
+
|
|
696
|
+
Assistant: ${clip(replyText, 2000) || '(no reply)'}
|
|
697
|
+
|
|
698
|
+
${observationsAsk()}
|
|
699
|
+
Respond with ONLY a JSON object {"observations": []}.`;
|
|
700
|
+
const r = await chatFn([{ role: 'user', content: prompt }]);
|
|
701
|
+
if (!r || !r.success || !r.json) return [];
|
|
702
|
+
const obs = (Array.isArray(r.json.observations) ? r.json.observations : []).map((o) => (o && project && PROJECT_KINDS.has(String(o.kind)) && !o.project ? { ...o, project } : o));
|
|
703
|
+
return noteObservations(obs, src);
|
|
704
|
+
} catch { return []; }
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** A bot's lessons from reflectOnRun, offered to the team. */
|
|
708
|
+
function shareLessons(lessons, { botId, botName, domain } = {}) {
|
|
709
|
+
if (!available()) return [];
|
|
710
|
+
return (Array.isArray(lessons) ? lessons : []).slice(0, 3).map((l) => {
|
|
711
|
+
if (!l || !l.situation || !l.strategy) return null;
|
|
712
|
+
return observe({ kind: 'lesson', text: `When ${clip(l.situation, 140)}: ${clip(l.strategy, 200)}`, certainty: 'implied', domain }, { botId, botName });
|
|
713
|
+
}).filter(Boolean);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ─── The tools ──────────────────────────────────────────────────────────────
|
|
717
|
+
|
|
718
|
+
/** remember: an agent saves (or forgets) one thing. */
|
|
719
|
+
function rememberTool(args, ctx = {}) {
|
|
720
|
+
if (!available()) return { ok: false, output: 'The Codeply brain is off on this machine.' };
|
|
721
|
+
const bot = ctx.botId ? (() => { try { return require('./bots.js').getBot(ctx.botId); } catch { return null; } })() : null;
|
|
722
|
+
const src = { botId: bot ? bot.id : '', botName: bot ? bot.name : '' };
|
|
723
|
+
if (args.forget) {
|
|
724
|
+
const ok = forget(String(args.forget).trim());
|
|
725
|
+
return ok ? { ok: true, output: 'Forgotten.', meta: { label: 'forgot' } } : { ok: false, output: `No memory with id "${args.forget}". Use recall to find its id.` };
|
|
726
|
+
}
|
|
727
|
+
const kind = String(args.kind || '').toLowerCase();
|
|
728
|
+
const project = args.project || (PROJECT_KINDS.has(kind) ? projectOfFolder(ctx.cwd) : '');
|
|
729
|
+
const r = observe({ ...args, certainty: args.certainty || 'stated', project }, src);
|
|
730
|
+
const say = {
|
|
731
|
+
added: 'Saved', reinforced: 'Already known; it is now surer', replaced: 'Updated (the old value is kept in its history)',
|
|
732
|
+
challenged: 'Kept the confirmed memory; this one is noted, and will replace it if it comes up again',
|
|
733
|
+
kept: 'Not changed: the user set this one themselves. Their disagreement is flagged for them to review', ignored: 'Not saved',
|
|
734
|
+
}[r.action] || 'Done';
|
|
735
|
+
const output = `${say}${r.why ? ` (${r.why})` : ''}.${r.memory ? ` [${r.memory.id}] ${r.memory.text}` : ''}`;
|
|
736
|
+
return { ok: r.action !== 'ignored', output, meta: { label: r.memory ? clip(r.memory.text, 80) : r.action, brain: { action: r.action, id: r.memory ? r.memory.id : '' } } };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** recall: look something up in the brain. */
|
|
740
|
+
function recallTool(args, ctx = {}) {
|
|
741
|
+
if (!available()) return { ok: false, output: 'The Codeply brain is off on this machine.' };
|
|
742
|
+
const query = String(args.query || '').trim();
|
|
743
|
+
const pk = knownKey(args.project || '');
|
|
744
|
+
const scopes = pk ? [`project:${pk}`] : null;
|
|
745
|
+
let hits = list({ scopes, text: query || undefined, limit: 40 });
|
|
746
|
+
if (!hits.length && query) {
|
|
747
|
+
const want = terms(query);
|
|
748
|
+
hits = list({ scopes, limit: 400 }).filter((m) => overlap(want, `${m.slot} ${m.value} ${m.text}`) > 0).slice(0, 25);
|
|
749
|
+
}
|
|
750
|
+
if (!hits.length) return { ok: true, output: `Nothing in the brain${query ? ` about "${query}"` : ''}${pk ? ` for ${pk}` : ''}.`, meta: { label: query || 'brain' } };
|
|
751
|
+
const out = hits.slice(0, 25).map((m) => `- [${m.id}] ${KINDS[m.kind] ? KINDS[m.kind].label : m.kind}${m.project ? ` (${m.project})` : ''}: ${m.text} (${m.status}${m.locked ? ', set by the user' : ''}, seen ${m.evidence}x)`).join('\n');
|
|
752
|
+
return { ok: true, output: out, meta: { label: query || 'brain' } };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ─── Coordination: handoffs between agents ──────────────────────────────────
|
|
756
|
+
|
|
757
|
+
/** Domains a job can belong to, and the words that point at each. */
|
|
758
|
+
const DOMAINS = {
|
|
759
|
+
calendar: ['calendar', 'schedule', 'scheduling', 'meeting', 'meetings', 'appointment', 'event', 'availability', 'booking', 'agenda'],
|
|
760
|
+
email: ['email', 'emails', 'inbox', 'mail', 'gmail', 'reply', 'replies', 'newsletter'],
|
|
761
|
+
reminders: ['reminder', 'reminders', 'remind', 'todo', 'to-do', 'deadline', 'follow up', 'follow-up'],
|
|
762
|
+
calls: ['call', 'calls', 'phone', 'voice', 'ring'],
|
|
763
|
+
texts: ['text', 'texts', 'sms', 'whatsapp', 'message', 'messages', 'slack', 'chat'],
|
|
764
|
+
research: ['research', 'search', 'find', 'compare', 'investigate', 'sources', 'read up', 'analysis'],
|
|
765
|
+
code: ['code', 'coding', 'build', 'bug', 'deploy', 'website', 'app', 'repo', 'github', 'frontend', 'backend', 'test'],
|
|
766
|
+
social: ['social', 'post', 'posts', 'twitter', 'linkedin', 'instagram', 'tiktok', 'content', 'video'],
|
|
767
|
+
health: ['health', 'fitness', 'sleep', 'steps', 'workout', 'activity', 'wellbeing'],
|
|
768
|
+
money: ['money', 'budget', 'invoice', 'invoices', 'payment', 'expense', 'finance', 'bill', 'bills'],
|
|
769
|
+
};
|
|
770
|
+
const DOMAIN_KEYS = Object.keys(DOMAINS);
|
|
771
|
+
|
|
772
|
+
/** The domains a bot covers: what it says it handles, else read from its job. */
|
|
773
|
+
function botDomains(bot) {
|
|
774
|
+
if (!bot) return [];
|
|
775
|
+
const own = Array.isArray(bot.handles) ? bot.handles.filter((d) => DOMAINS[d]) : [];
|
|
776
|
+
if (own.length) return own;
|
|
777
|
+
// A lead's "Hand money matters to Hisaab" names a job it gives away, not one it does.
|
|
778
|
+
const kept = String(bot.instructions || '').split(/(?<=[.!?;\n])/).filter((s) => !/\b(hand|pass|forward|send|give|leave|delegate|route)\b[^.!?;\n]*\bto\b/i.test(s)).join(' ');
|
|
779
|
+
const hay = ` ${norm(`${bot.name} ${bot.specialty} ${kept}`)} `;
|
|
780
|
+
return DOMAIN_KEYS.filter((d) => DOMAINS[d].some((w) => hay.includes(` ${norm(w)} `)));
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Who a handoff goes to: a bot by name, else the bot whose domains fit best.
|
|
785
|
+
* @returns {{ bot: object|null, domain: string }}
|
|
786
|
+
*/
|
|
787
|
+
function routeHandoff(to, team, { fromBotId, intent } = {}) {
|
|
788
|
+
const bots = (team || []).filter((b) => b && b.id !== fromBotId);
|
|
789
|
+
const want = norm(to);
|
|
790
|
+
const byName = bots.find((b) => norm(b.name) === want || b.id === to);
|
|
791
|
+
if (byName) return { bot: byName, domain: botDomains(byName)[0] || '' };
|
|
792
|
+
const domain = DOMAINS[want] ? want : DOMAIN_KEYS.find((d) => DOMAINS[d].some((w) => norm(w) === want)) || '';
|
|
793
|
+
const hay = ` ${norm(`${to} ${intent}`)} `;
|
|
794
|
+
const scored = bots.map((b) => {
|
|
795
|
+
const doms = botDomains(b);
|
|
796
|
+
let s = domain && doms.includes(domain) ? 5 : 0;
|
|
797
|
+
for (const d of doms) for (const w of DOMAINS[d]) if (hay.includes(` ${norm(w)} `)) s += 1;
|
|
798
|
+
if (b.role === 'orchestrator') s -= 0.5;
|
|
799
|
+
return { b, s };
|
|
800
|
+
}).filter((x) => x.s > 0).sort((a, b) => b.s - a.s);
|
|
801
|
+
return { bot: scored.length ? scored[0].b : null, domain };
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const rowToHandoff = (r) => r && {
|
|
805
|
+
id: r.id, fromBot: r.from_bot || '', fromName: r.from_name || '', toBot: r.to_bot || '', toName: r.to_name || '', domain: r.domain || '',
|
|
806
|
+
intent: r.intent, facts: json(r.facts, []), decision: r.decision || '', requester: r.requester, origin: json(r.origin, null),
|
|
807
|
+
parent: r.parent || '', taskId: r.task_id || '', status: r.status, result: r.result || '', createdAt: Number(r.created_at), updatedAt: Number(r.updated_at),
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* One agent passes a job to another. Only what the next agent needs travels:
|
|
812
|
+
* the intent, the facts, the decision so far, and who asked for it in the end.
|
|
813
|
+
*
|
|
814
|
+
* requester stays what it was at the start of the chain: 'user' only when the
|
|
815
|
+
* user asked. A meeting request from someone's email is 'other', so whatever
|
|
816
|
+
* the calendar bot then wants to do waits for the user's OK (autopilot.js).
|
|
817
|
+
*/
|
|
818
|
+
function createHandoff({ fromBot, to, intent, facts, decision, requester, origin, parent, team }) {
|
|
819
|
+
if (!open()) return { error: 'The coordination board is not available on this machine.' };
|
|
820
|
+
const why = clip(intent, 500);
|
|
821
|
+
if (!why) return { error: 'A handoff needs an intent: what the next agent should get done.' };
|
|
822
|
+
// A chain stays short, and the same job never bounces between bots. Passing a
|
|
823
|
+
// NEW job back (the calendar bot booked it, the email bot sends the invite) is fine.
|
|
824
|
+
const chain = []; const seen = new Set();
|
|
825
|
+
for (let p = parent ? getHandoff(parent) : null; p && !seen.has(p.id) && chain.length < 10; p = p.parent ? getHandoff(p.parent) : null) { seen.add(p.id); chain.push(p); }
|
|
826
|
+
if (chain.length >= MAX_HANDOFF_CHAIN) return { error: `This work has been handed on ${chain.length} times already. Finish it yourself or tell the user what is left.` };
|
|
827
|
+
const route = routeHandoff(to, team || [], { fromBotId: fromBot && fromBot.id, intent: why });
|
|
828
|
+
const bounce = route.bot && chain.find((h) => h.fromBot === route.bot.id && sameJob(h.intent, why));
|
|
829
|
+
if (bounce) return { error: `${route.bot.name} handed you this job; passing the same job back would loop. Do it yourself, or say what blocks it.` };
|
|
830
|
+
const t = now();
|
|
831
|
+
const id = newId('h', t);
|
|
832
|
+
const list = (Array.isArray(facts) ? facts : String(facts || '').split(/\n+/)).map((f) => clip(String(f).replace(/^[-*\s]+/, ''), 300)).filter((f) => f && !SECRET_RE.test(f)).slice(0, 12);
|
|
833
|
+
q.insertHandoff.run(id, fromBot ? fromBot.id : null, fromBot ? clip(fromBot.name, 40) : 'Codeply', route.bot ? route.bot.id : null, route.bot ? clip(route.bot.name, 40) : null,
|
|
834
|
+
route.domain || null, why, JSON.stringify(list), clip(decision, 400) || null, requester === 'user' ? 'user' : 'other',
|
|
835
|
+
origin ? JSON.stringify(origin).slice(0, 2000) : null, parent || null, null, route.bot ? 'open' : 'unrouted', null, t, t);
|
|
836
|
+
return { handoff: getHandoff(id) };
|
|
837
|
+
}
|
|
838
|
+
/** The same job in other words: most of its meaningful words are shared. */
|
|
839
|
+
function sameJob(a, b) {
|
|
840
|
+
const x = terms(a); const y = terms(b);
|
|
841
|
+
if (!x.length || !y.length) return false;
|
|
842
|
+
return overlap(x, y) / Math.min(x.length, y.length) >= 0.6;
|
|
843
|
+
}
|
|
844
|
+
const getHandoff = (id) => (open() ? rowToHandoff(q.getHandoff.get(String(id))) : null);
|
|
845
|
+
function updateHandoff(id, patch = {}) {
|
|
846
|
+
const cur = getHandoff(id);
|
|
847
|
+
if (!cur) return null;
|
|
848
|
+
q.updateHandoff.run(patch.toBot !== undefined ? patch.toBot : cur.toBot || null, patch.toName !== undefined ? patch.toName : cur.toName || null,
|
|
849
|
+
patch.taskId !== undefined ? patch.taskId : cur.taskId || null, patch.status || cur.status, patch.result !== undefined ? clip(patch.result, 1200) : cur.result || null, now(), cur.id);
|
|
850
|
+
return getHandoff(cur.id);
|
|
851
|
+
}
|
|
852
|
+
const handoffsByStatus = (status, limit = 50) => (open() ? q.handoffsByStatus.all(String(status), limit).map(rowToHandoff) : []);
|
|
853
|
+
const recentHandoffs = (limit = 30) => (open() ? q.recentHandoffs.all(limit).map(rowToHandoff) : []);
|
|
854
|
+
const handoffForTask = (taskId) => (open() ? rowToHandoff(q.handoffByTask.get(String(taskId))) : null);
|
|
855
|
+
|
|
856
|
+
/** The task text the receiving agent works from. */
|
|
857
|
+
function handoffTask(h) {
|
|
858
|
+
return `HANDOFF from ${h.fromName || 'Codeply'}${h.domain ? ` (for ${h.domain})` : ''}
|
|
859
|
+
Get done: ${h.intent}
|
|
860
|
+
${h.facts.length ? `What you need to know:\n${h.facts.map((f) => `- ${f}`).join('\n')}\n` : ''}${h.decision ? `Decided so far: ${h.decision}\n` : ''}${h.requester === 'user' ? 'The user asked for this.' : 'This came from someone other than the user (for example an email they received): check it against the user\'s calendar, preferences and commitments, and only propose; anything that commits the user waits for their OK.'}
|
|
861
|
+
Do your part only; ${h.fromName || 'the sender'} has done theirs. If something conflicts, say exactly what and offer the best alternative.`;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// The host (Craft's autopilot) turns an open handoff into a task for its bot.
|
|
865
|
+
let coordinator = null;
|
|
866
|
+
function setCoordinator(fn) { coordinator = typeof fn === 'function' ? fn : null; }
|
|
867
|
+
const canHandOff = () => !!coordinator && !!open();
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Hand a job to the right agent and move on (no waiting, unlike ask_bot).
|
|
871
|
+
* opts: { fromBot, requester, origin, parent, team }
|
|
872
|
+
*/
|
|
873
|
+
async function handOff(args, opts = {}) {
|
|
874
|
+
if (!canHandOff()) return { ok: false, output: 'Handing work to another bot needs the Craft app running.' };
|
|
875
|
+
let team = opts.team;
|
|
876
|
+
if (!team) { try { team = require('./bots.js').listBots(); } catch { team = []; } }
|
|
877
|
+
const r = createHandoff({ fromBot: opts.fromBot || null, to: args.to || args.bot || args.domain, intent: args.intent || args.task, facts: args.facts, decision: args.decision, requester: opts.requester || 'user', origin: opts.origin, parent: opts.parent, team });
|
|
878
|
+
if (r.error) return { ok: false, output: r.error };
|
|
879
|
+
const h = r.handoff;
|
|
880
|
+
if (h.status === 'unrouted') {
|
|
881
|
+
return { ok: false, output: `No bot handles "${args.to || '?'}". Bots: ${team.map((b) => `${b.name} (${botDomains(b).join(', ') || 'general'})`).join('; ') || 'none'}. Use a bot's name, or do it yourself.`, meta: { label: 'no bot for that' } };
|
|
882
|
+
}
|
|
883
|
+
let queued = null;
|
|
884
|
+
try { queued = await coordinator(h); } catch (e) { queued = { error: e.message }; }
|
|
885
|
+
if (!queued || queued.error) {
|
|
886
|
+
updateHandoff(h.id, { status: 'failed', result: (queued && queued.error) || 'could not be queued' });
|
|
887
|
+
return { ok: false, output: `Could not hand it to ${h.toName}: ${(queued && queued.error) || 'not queued'}.` };
|
|
888
|
+
}
|
|
889
|
+
updateHandoff(h.id, { status: 'queued', taskId: queued.taskId || null });
|
|
890
|
+
return { ok: true, output: `Handed to ${h.toName}${h.domain ? ` (${h.domain})` : ''}. It works on it now; the user hears the result. Do not do that part yourself.`, meta: { label: `to ${h.toName}: ${clip(h.intent, 80)}`, handoff: { id: h.id, to: h.toName } } };
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// ─── Spotting work for another agent ────────────────────────────────────────
|
|
894
|
+
|
|
895
|
+
const MEETING_RE = /\b(meet(ing)?|call|catch[- ]?up|sync|appointment|interview|demo|lunch|dinner|coffee|available|availability|free (at|on)|reschedul\w*|invite|calendar|slot|book(ed|ing)?)\b/i;
|
|
896
|
+
const WHEN_RE = /\b(today|tonight|tomorrow|monday|tuesday|wednesday|thursday|friday|saturday|sunday|next week|this week|(at|by|around) \d{1,2}(:\d{2})?\s*(am|pm|a\.m\.|p\.m\.)?|\d{1,2}(:\d{2})?\s*(am|pm|a\.m\.|p\.m\.)|\d{1,2}:\d{2}|\d{1,2}(st|nd|rd|th)? (of )?(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec))\b/i;
|
|
897
|
+
|
|
898
|
+
/** An email asking for the user's time: a meeting word and a time. No model call. */
|
|
899
|
+
function looksLikeScheduling(subject, text) {
|
|
900
|
+
const s = `${subject || ''} ${text || ''}`;
|
|
901
|
+
return MEETING_RE.test(s) && WHEN_RE.test(s);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
module.exports = {
|
|
905
|
+
KINDS, KIND_KEYS, CERTAINTY, DOMAINS, DOMAIN_KEYS, SECRET_RE,
|
|
906
|
+
configure, usable, available, enabled, setEnabled, close, prune,
|
|
907
|
+
observe, cleanObservation, get, list, correct, confirm, resolve, forget, clear, changes,
|
|
908
|
+
touchProject, projects, projectFor, projectsNamed, projectKey, knownKey, projectOfFolder,
|
|
909
|
+
contextBlock, teamLessons, observationsAsk, noteObservations, learnFromExchange, shareLessons,
|
|
910
|
+
rememberTool, recallTool, REMEMBER_FORMAT,
|
|
911
|
+
botDomains, routeHandoff, createHandoff, getHandoff, updateHandoff, handoffsByStatus, recentHandoffs, handoffForTask, handoffTask,
|
|
912
|
+
setCoordinator, canHandOff, handOff, looksLikeScheduling,
|
|
913
|
+
terms, sameText,
|
|
914
|
+
};
|