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
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The life store: what the user's bots know about their day, in one local
|
|
3
|
+
* SQLite file (~/.codeply/life/life.db). Every bot reads the same store, so
|
|
4
|
+
* they share one picture of the user's life instead of each knowing only its
|
|
5
|
+
* own thread.
|
|
6
|
+
*
|
|
7
|
+
* items Things that happened or will happen: calendar events, email in
|
|
8
|
+
* and out, calls and their summaries, what the user has open on
|
|
9
|
+
* screen, Orbit questions, plans, bot mail, Slack messages.
|
|
10
|
+
* id = "<source>:<external id>", so a source can upsert the same
|
|
11
|
+
* thing again as it changes.
|
|
12
|
+
* people Who the user deals with: names, addresses, how important they
|
|
13
|
+
* are, when each side last wrote.
|
|
14
|
+
* state Small named values (a source's cursor, the current activity).
|
|
15
|
+
* nudges What the bots reached out about, and the user's feedback on it.
|
|
16
|
+
* tasks Work the bots took on by themselves (the autonomy loop).
|
|
17
|
+
* outbox Outward actions waiting out their hold time.
|
|
18
|
+
* audit Append-only: every action a bot took on its own.
|
|
19
|
+
*
|
|
20
|
+
* Sensitive items (call transcripts, private mail bodies) keep only their
|
|
21
|
+
* title in clear text; body and data are sealed with AES-256-GCM under a key
|
|
22
|
+
* the host keeps (Craft: a random key protected by Windows' own data
|
|
23
|
+
* protection, safeStorage). With no key, sensitive bodies are not stored.
|
|
24
|
+
*
|
|
25
|
+
* Same SQLite drivers as session-db.js (node:sqlite, else better-sqlite3).
|
|
26
|
+
*/
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const crypto = require('crypto');
|
|
31
|
+
const { loadDriver } = require('./session-db.js');
|
|
32
|
+
|
|
33
|
+
const SCHEMA_VERSION = 1;
|
|
34
|
+
const DAY = 86400000;
|
|
35
|
+
// How long things are kept. Screen activity is the noisiest, sensitive things the most private.
|
|
36
|
+
const KEEP_DAYS = { default: 90, activity: 14, screen: 3, sensitive: 30, audit: 365 };
|
|
37
|
+
|
|
38
|
+
function defaultFile() {
|
|
39
|
+
return process.env.CODEPLY_LIFE_DB || path.join(os.homedir(), '.codeply', 'life', 'life.db');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Sealing ────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function sealer(key) {
|
|
45
|
+
if (!key) return null;
|
|
46
|
+
const k = Buffer.isBuffer(key) ? key : Buffer.from(String(key), 'base64');
|
|
47
|
+
if (k.length !== 32) throw new Error('The life store key must be 32 bytes.');
|
|
48
|
+
return {
|
|
49
|
+
seal(text) {
|
|
50
|
+
const iv = crypto.randomBytes(12);
|
|
51
|
+
const c = crypto.createCipheriv('aes-256-gcm', k, iv);
|
|
52
|
+
const ct = Buffer.concat([c.update(String(text), 'utf8'), c.final()]);
|
|
53
|
+
return `v1:${Buffer.concat([iv, c.getAuthTag(), ct]).toString('base64')}`;
|
|
54
|
+
},
|
|
55
|
+
open(blob) {
|
|
56
|
+
const s = String(blob || '');
|
|
57
|
+
if (!s.startsWith('v1:')) return s;
|
|
58
|
+
const b = Buffer.from(s.slice(3), 'base64');
|
|
59
|
+
const d = crypto.createDecipheriv('aes-256-gcm', k, b.subarray(0, 12));
|
|
60
|
+
d.setAuthTag(b.subarray(12, 28));
|
|
61
|
+
return Buffer.concat([d.update(b.subarray(28)), d.final()]).toString('utf8');
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const clip = (s, n) => String(s == null ? '' : s).slice(0, n);
|
|
67
|
+
const json = (v, fallback) => { try { return v == null ? fallback : JSON.parse(v); } catch { return fallback; } };
|
|
68
|
+
const lower = (s) => String(s || '').trim().toLowerCase();
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {{ file?: string, key?: Buffer|string, modulePaths?: string[], now?: () => number }} [opts]
|
|
72
|
+
* @returns the store, or null when no SQLite driver loads on this machine.
|
|
73
|
+
*/
|
|
74
|
+
function openLifeStore(opts = {}) {
|
|
75
|
+
const driver = loadDriver(opts.modulePaths);
|
|
76
|
+
if (!driver) return null;
|
|
77
|
+
const file = opts.file || defaultFile();
|
|
78
|
+
const now = opts.now || Date.now;
|
|
79
|
+
const box = sealer(opts.key);
|
|
80
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
81
|
+
const db = driver.open(file);
|
|
82
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
83
|
+
db.exec('PRAGMA synchronous = NORMAL');
|
|
84
|
+
db.exec(`
|
|
85
|
+
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
86
|
+
CREATE TABLE IF NOT EXISTS items (
|
|
87
|
+
id TEXT PRIMARY KEY, source TEXT NOT NULL, kind TEXT NOT NULL,
|
|
88
|
+
at INTEGER NOT NULL, end_at INTEGER, title TEXT NOT NULL DEFAULT '', body TEXT,
|
|
89
|
+
people TEXT NOT NULL DEFAULT '[]', place TEXT, url TEXT, data TEXT,
|
|
90
|
+
sensitive INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL
|
|
91
|
+
);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS items_at ON items (at);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS items_source ON items (source, at);
|
|
94
|
+
CREATE TABLE IF NOT EXISTS people (
|
|
95
|
+
id TEXT PRIMARY KEY, name TEXT NOT NULL DEFAULT '', emails TEXT NOT NULL DEFAULT '[]',
|
|
96
|
+
importance REAL NOT NULL DEFAULT 0, last_from INTEGER, last_to INTEGER, note TEXT, updated_at INTEGER NOT NULL
|
|
97
|
+
);
|
|
98
|
+
CREATE TABLE IF NOT EXISTS state (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL);
|
|
99
|
+
CREATE TABLE IF NOT EXISTS nudges (
|
|
100
|
+
id TEXT PRIMARY KEY, trigger TEXT NOT NULL, key TEXT NOT NULL, bot_id TEXT, how TEXT, text TEXT,
|
|
101
|
+
at INTEGER NOT NULL, feedback TEXT, feedback_at INTEGER
|
|
102
|
+
);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS nudges_key ON nudges (key, at);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS nudges_at ON nudges (at);
|
|
105
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
106
|
+
id TEXT PRIMARY KEY, bot_id TEXT, source TEXT NOT NULL, requester TEXT NOT NULL, text TEXT NOT NULL,
|
|
107
|
+
status TEXT NOT NULL, data TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
108
|
+
);
|
|
109
|
+
CREATE INDEX IF NOT EXISTS tasks_status ON tasks (status, created_at);
|
|
110
|
+
CREATE TABLE IF NOT EXISTS outbox (
|
|
111
|
+
id TEXT PRIMARY KEY, task_id TEXT, bot_id TEXT, tool TEXT NOT NULL, title TEXT NOT NULL, detail TEXT,
|
|
112
|
+
due_at INTEGER NOT NULL, status TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
113
|
+
);
|
|
114
|
+
CREATE TABLE IF NOT EXISTS audit (
|
|
115
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT, at INTEGER NOT NULL, bot_id TEXT, task_id TEXT, kind TEXT NOT NULL,
|
|
116
|
+
tool TEXT, title TEXT, detail TEXT, verdict TEXT, ok INTEGER, result TEXT
|
|
117
|
+
);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS audit_at ON audit (at);
|
|
119
|
+
`);
|
|
120
|
+
db.prepare('INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)').run('schema', String(SCHEMA_VERSION));
|
|
121
|
+
|
|
122
|
+
const q = {
|
|
123
|
+
upsertItem: db.prepare(`INSERT INTO items (id, source, kind, at, end_at, title, body, people, place, url, data, sensitive, updated_at)
|
|
124
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
125
|
+
ON CONFLICT(id) DO UPDATE SET source=excluded.source, kind=excluded.kind, at=excluded.at, end_at=excluded.end_at,
|
|
126
|
+
title=excluded.title, body=excluded.body, people=excluded.people, place=excluded.place, url=excluded.url,
|
|
127
|
+
data=excluded.data, sensitive=excluded.sensitive, updated_at=excluded.updated_at`),
|
|
128
|
+
getItem: db.prepare('SELECT * FROM items WHERE id = ?'),
|
|
129
|
+
deleteItem: db.prepare('DELETE FROM items WHERE id = ?'),
|
|
130
|
+
deleteSource: db.prepare('DELETE FROM items WHERE source = ?'),
|
|
131
|
+
countSource: db.prepare('SELECT source, COUNT(*) AS n, MAX(updated_at) AS last FROM items GROUP BY source'),
|
|
132
|
+
upsertPerson: db.prepare(`INSERT INTO people (id, name, emails, importance, last_from, last_to, note, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
133
|
+
ON CONFLICT(id) DO UPDATE SET name=excluded.name, emails=excluded.emails, importance=excluded.importance,
|
|
134
|
+
last_from=excluded.last_from, last_to=excluded.last_to, note=excluded.note, updated_at=excluded.updated_at`),
|
|
135
|
+
getPerson: db.prepare('SELECT * FROM people WHERE id = ?'),
|
|
136
|
+
findPeople: db.prepare("SELECT * FROM people WHERE name LIKE ? ESCAPE '\\' OR emails LIKE ? ESCAPE '\\' ORDER BY importance DESC LIMIT ?"),
|
|
137
|
+
topPeople: db.prepare('SELECT * FROM people ORDER BY importance DESC, updated_at DESC LIMIT ?'),
|
|
138
|
+
getState: db.prepare('SELECT value FROM state WHERE key = ?'),
|
|
139
|
+
setState: db.prepare('INSERT INTO state (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at'),
|
|
140
|
+
deleteState: db.prepare('DELETE FROM state WHERE key = ?'),
|
|
141
|
+
addNudge: db.prepare('INSERT OR REPLACE INTO nudges (id, trigger, key, bot_id, how, text, at, feedback, feedback_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'),
|
|
142
|
+
nudgeFeedback: db.prepare('UPDATE nudges SET feedback = ?, feedback_at = ? WHERE id = ?'),
|
|
143
|
+
getNudge: db.prepare('SELECT * FROM nudges WHERE id = ?'),
|
|
144
|
+
lastNudgeFor: db.prepare('SELECT * FROM nudges WHERE key = ? ORDER BY at DESC LIMIT 1'),
|
|
145
|
+
nudgesSince: db.prepare('SELECT * FROM nudges WHERE at >= ? ORDER BY at DESC LIMIT ?'),
|
|
146
|
+
upsertTask: db.prepare(`INSERT INTO tasks (id, bot_id, source, requester, text, status, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
147
|
+
ON CONFLICT(id) DO UPDATE SET bot_id=excluded.bot_id, status=excluded.status, data=excluded.data, updated_at=excluded.updated_at`),
|
|
148
|
+
getTask: db.prepare('SELECT * FROM tasks WHERE id = ?'),
|
|
149
|
+
tasksByStatus: db.prepare('SELECT * FROM tasks WHERE status = ? ORDER BY created_at ASC LIMIT ?'),
|
|
150
|
+
recentTasks: db.prepare('SELECT * FROM tasks ORDER BY updated_at DESC LIMIT ?'),
|
|
151
|
+
upsertOutbox: db.prepare(`INSERT INTO outbox (id, task_id, bot_id, tool, title, detail, due_at, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
152
|
+
ON CONFLICT(id) DO UPDATE SET status=excluded.status, due_at=excluded.due_at, updated_at=excluded.updated_at`),
|
|
153
|
+
getOutbox: db.prepare('SELECT * FROM outbox WHERE id = ?'),
|
|
154
|
+
outboxByStatus: db.prepare('SELECT * FROM outbox WHERE status = ? ORDER BY due_at ASC LIMIT ?'),
|
|
155
|
+
addAudit: db.prepare('INSERT INTO audit (at, bot_id, task_id, kind, tool, title, detail, verdict, ok, result) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'),
|
|
156
|
+
auditSince: db.prepare('SELECT * FROM audit WHERE at >= ? ORDER BY seq DESC LIMIT ?'),
|
|
157
|
+
auditCount: db.prepare('SELECT COUNT(*) AS n FROM audit WHERE at >= ? AND kind = ? AND (bot_id = ? OR ? IS NULL)'),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// ─── Items ────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
function rowToItem(r) {
|
|
163
|
+
if (!r) return null;
|
|
164
|
+
const sensitive = !!r.sensitive;
|
|
165
|
+
const open = (v) => {
|
|
166
|
+
if (v == null) return null;
|
|
167
|
+
if (!sensitive) return v;
|
|
168
|
+
if (!box) return null;
|
|
169
|
+
try { return box.open(v); } catch { return null; }
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
id: r.id, source: r.source, kind: r.kind, at: Number(r.at), end: r.end_at == null ? null : Number(r.end_at),
|
|
173
|
+
title: r.title, body: open(r.body) || '', people: json(r.people, []), place: r.place || '', url: r.url || '',
|
|
174
|
+
data: json(open(r.data), {}) || {}, sensitive, updatedAt: Number(r.updated_at),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* item: { id, source, kind, at, end?, title, body?, people?, place?, url?, data?, sensitive? }
|
|
180
|
+
* A sensitive item without a key keeps its title only.
|
|
181
|
+
*/
|
|
182
|
+
function upsertItem(item) {
|
|
183
|
+
if (!item || !item.id || !item.source || !item.kind) throw new Error('A life item needs id, source and kind.');
|
|
184
|
+
const sensitive = !!item.sensitive;
|
|
185
|
+
const seal = (v) => (v == null || v === '' ? null : sensitive ? (box ? box.seal(v) : null) : v);
|
|
186
|
+
const data = item.data && Object.keys(item.data).length ? JSON.stringify(item.data) : null;
|
|
187
|
+
q.upsertItem.run(
|
|
188
|
+
clip(item.id, 300), clip(item.source, 40), clip(item.kind, 40), Math.round(Number(item.at) || now()),
|
|
189
|
+
item.end == null ? null : Math.round(Number(item.end)), clip(item.title, 500), seal(item.body ? clip(item.body, 40000) : null),
|
|
190
|
+
JSON.stringify((Array.isArray(item.people) ? item.people : []).map((p) => clip(p, 200)).slice(0, 40)),
|
|
191
|
+
item.place ? clip(item.place, 300) : null, item.url ? clip(item.url, 1000) : null, seal(data), sensitive ? 1 : 0, now(),
|
|
192
|
+
);
|
|
193
|
+
return getItem(item.id);
|
|
194
|
+
}
|
|
195
|
+
const getItem = (id) => rowToItem(q.getItem.get(String(id)));
|
|
196
|
+
|
|
197
|
+
/** Items by time window, source, kind; newest first unless order: 'asc'. */
|
|
198
|
+
function items({ from, to, sources, kinds, limit = 50, order = 'desc', text } = {}) {
|
|
199
|
+
const where = []; const args = [];
|
|
200
|
+
if (from != null) { where.push('(at >= ? OR (end_at IS NOT NULL AND end_at >= ?))'); args.push(Math.round(from), Math.round(from)); }
|
|
201
|
+
if (to != null) { where.push('at <= ?'); args.push(Math.round(to)); }
|
|
202
|
+
if (sources && sources.length) { where.push(`source IN (${sources.map(() => '?').join(',')})`); args.push(...sources); }
|
|
203
|
+
if (kinds && kinds.length) { where.push(`kind IN (${kinds.map(() => '?').join(',')})`); args.push(...kinds); }
|
|
204
|
+
if (text) {
|
|
205
|
+
const like = `%${String(text).replace(/[\\%_]/g, (c) => `\\${c}`)}%`;
|
|
206
|
+
where.push("(title LIKE ? ESCAPE '\\' OR (sensitive = 0 AND body LIKE ? ESCAPE '\\') OR people LIKE ? ESCAPE '\\' OR place LIKE ? ESCAPE '\\')");
|
|
207
|
+
args.push(like, like, like, like);
|
|
208
|
+
}
|
|
209
|
+
const sql = `SELECT * FROM items${where.length ? ` WHERE ${where.join(' AND ')}` : ''} ORDER BY at ${order === 'asc' ? 'ASC' : 'DESC'} LIMIT ?`;
|
|
210
|
+
args.push(Math.max(1, Math.min(500, Number(limit) || 50)));
|
|
211
|
+
return db.prepare(sql).all(...args).map(rowToItem);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Words anywhere (titles, bodies, people, places), newest first. Sensitive bodies are opened in memory. */
|
|
215
|
+
function search(text, { limit = 20, sources } = {}) {
|
|
216
|
+
const t = String(text || '').trim();
|
|
217
|
+
if (!t) return [];
|
|
218
|
+
const words = t.toLowerCase().split(/\s+/).filter((w) => w.length > 1).slice(0, 6);
|
|
219
|
+
const pool = new Map();
|
|
220
|
+
for (const w of words.length ? words : [t.toLowerCase()]) {
|
|
221
|
+
for (const it of items({ text: w, limit: 200, sources })) pool.set(it.id, it);
|
|
222
|
+
}
|
|
223
|
+
// Sensitive bodies are not searchable in SQL: open the recent ones and look.
|
|
224
|
+
if (box) {
|
|
225
|
+
for (const r of db.prepare('SELECT * FROM items WHERE sensitive = 1 ORDER BY at DESC LIMIT 300').all()) {
|
|
226
|
+
const it = rowToItem(r);
|
|
227
|
+
if (sources && sources.length && !sources.includes(it.source)) continue;
|
|
228
|
+
if (words.some((w) => it.body.toLowerCase().includes(w))) pool.set(it.id, it);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const score = (it) => {
|
|
232
|
+
const hay = `${it.title} ${it.body} ${it.people.join(' ')} ${it.place}`.toLowerCase();
|
|
233
|
+
return words.reduce((n, w) => n + (hay.includes(w) ? 1 : 0), 0);
|
|
234
|
+
};
|
|
235
|
+
return [...pool.values()].map((it) => ({ it, s: score(it) }))
|
|
236
|
+
.sort((a, b) => b.s - a.s || b.it.at - a.it.at).slice(0, limit).map((x) => x.it);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── People ───────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
const personId = (p) => (p.email ? lower(p.email) : `name:${lower(p.name)}`);
|
|
242
|
+
function rowToPerson(r) {
|
|
243
|
+
return r && { id: r.id, name: r.name, emails: json(r.emails, []), importance: Number(r.importance) || 0, lastFrom: r.last_from == null ? null : Number(r.last_from), lastTo: r.last_to == null ? null : Number(r.last_to), note: r.note || '' };
|
|
244
|
+
}
|
|
245
|
+
/** Merge what a source learned about someone: { email?, name?, importance?, lastFrom?, lastTo?, note? }. */
|
|
246
|
+
function notePerson(p) {
|
|
247
|
+
if (!p || (!p.email && !p.name)) return null;
|
|
248
|
+
const id = personId(p);
|
|
249
|
+
const cur = rowToPerson(q.getPerson.get(id)) || { name: '', emails: [], importance: 0, lastFrom: null, lastTo: null, note: '' };
|
|
250
|
+
const emails = [...new Set([...cur.emails, ...(p.email ? [lower(p.email)] : [])])].slice(0, 10);
|
|
251
|
+
const max = (a, b) => (a == null ? b : b == null ? a : Math.max(a, b));
|
|
252
|
+
const next = {
|
|
253
|
+
name: clip(p.name || cur.name, 120), emails,
|
|
254
|
+
importance: p.importance == null ? cur.importance : Math.max(0, Math.min(10, Number(p.importance))),
|
|
255
|
+
lastFrom: max(cur.lastFrom, p.lastFrom == null ? null : Number(p.lastFrom)),
|
|
256
|
+
lastTo: max(cur.lastTo, p.lastTo == null ? null : Number(p.lastTo)),
|
|
257
|
+
note: clip(p.note || cur.note, 400),
|
|
258
|
+
};
|
|
259
|
+
q.upsertPerson.run(id, next.name, JSON.stringify(next.emails), next.importance, next.lastFrom, next.lastTo, next.note || null, now());
|
|
260
|
+
return { id, ...next };
|
|
261
|
+
}
|
|
262
|
+
const person = (emailOrName) => rowToPerson(q.getPerson.get(String(emailOrName).includes('@') ? lower(emailOrName) : `name:${lower(emailOrName)}`));
|
|
263
|
+
function findPeople(text, limit = 5) {
|
|
264
|
+
const like = `%${String(text || '').replace(/[\\%_]/g, (c) => `\\${c}`)}%`;
|
|
265
|
+
return q.findPeople.all(like, like, limit).map(rowToPerson);
|
|
266
|
+
}
|
|
267
|
+
const topPeople = (limit = 10) => q.topPeople.all(limit).map(rowToPerson);
|
|
268
|
+
|
|
269
|
+
// ─── State ────────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
const getState = (key, fallback = null) => { const r = q.getState.get(String(key)); return r ? json(r.value, fallback) : fallback; };
|
|
272
|
+
const setState = (key, value) => { q.setState.run(String(key), JSON.stringify(value), now()); return value; };
|
|
273
|
+
const deleteState = (key) => { q.deleteState.run(String(key)); };
|
|
274
|
+
|
|
275
|
+
// ─── Nudges ───────────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
const rowToNudge = (r) => r && { id: r.id, trigger: r.trigger, key: r.key, botId: r.bot_id, how: r.how, text: r.text, at: Number(r.at), feedback: r.feedback || null, feedbackAt: r.feedback_at == null ? null : Number(r.feedback_at) };
|
|
278
|
+
function addNudge(n) {
|
|
279
|
+
const id = n.id || `n${now().toString(36)}${crypto.randomBytes(3).toString('hex')}`;
|
|
280
|
+
q.addNudge.run(id, clip(n.trigger, 40), clip(n.key, 300), n.botId || null, n.how || null, clip(n.text, 600), Math.round(n.at || now()), null, null);
|
|
281
|
+
return rowToNudge(q.getNudge.get(id));
|
|
282
|
+
}
|
|
283
|
+
const nudgeFeedback = (id, feedback) => { q.nudgeFeedback.run(String(feedback), now(), String(id)); return rowToNudge(q.getNudge.get(String(id))); };
|
|
284
|
+
const getNudge = (id) => rowToNudge(q.getNudge.get(String(id)));
|
|
285
|
+
const lastNudgeFor = (key) => rowToNudge(q.lastNudgeFor.get(String(key)));
|
|
286
|
+
const nudgesSince = (since, limit = 100) => q.nudgesSince.all(Math.round(since), limit).map(rowToNudge);
|
|
287
|
+
|
|
288
|
+
// ─── Tasks, outbox, audit (the autonomy loop) ─────────────────────────────
|
|
289
|
+
|
|
290
|
+
const rowToTask = (r) => r && { id: r.id, botId: r.bot_id, source: r.source, requester: r.requester, text: r.text, status: r.status, data: json(r.data, {}) || {}, createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };
|
|
291
|
+
function saveTask(t) {
|
|
292
|
+
const id = t.id || `t${now().toString(36)}${crypto.randomBytes(3).toString('hex')}`;
|
|
293
|
+
const cur = rowToTask(q.getTask.get(id));
|
|
294
|
+
q.upsertTask.run(id, t.botId || (cur && cur.botId) || null, clip(t.source || (cur && cur.source) || 'crew', 30), clip(t.requester || (cur && cur.requester) || 'user', 30),
|
|
295
|
+
clip(t.text || (cur && cur.text) || '', 4000), clip(t.status || (cur && cur.status) || 'queued', 20),
|
|
296
|
+
JSON.stringify({ ...((cur && cur.data) || {}), ...(t.data || {}) }).slice(0, 60000), cur ? cur.createdAt : now(), now());
|
|
297
|
+
return rowToTask(q.getTask.get(id));
|
|
298
|
+
}
|
|
299
|
+
const getTask = (id) => rowToTask(q.getTask.get(String(id)));
|
|
300
|
+
const tasksByStatus = (status, limit = 50) => q.tasksByStatus.all(String(status), limit).map(rowToTask);
|
|
301
|
+
const recentTasks = (limit = 30) => q.recentTasks.all(limit).map(rowToTask);
|
|
302
|
+
|
|
303
|
+
const rowToOut = (r) => r && { id: r.id, taskId: r.task_id, botId: r.bot_id, tool: r.tool, title: r.title, detail: r.detail || '', dueAt: Number(r.due_at), status: r.status, createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };
|
|
304
|
+
function saveOutbox(o) {
|
|
305
|
+
const id = o.id || `o${now().toString(36)}${crypto.randomBytes(3).toString('hex')}`;
|
|
306
|
+
const cur = rowToOut(q.getOutbox.get(id));
|
|
307
|
+
q.upsertOutbox.run(id, o.taskId || (cur && cur.taskId) || null, o.botId || (cur && cur.botId) || null, clip(o.tool || (cur && cur.tool), 60), clip(o.title || (cur && cur.title) || '', 300),
|
|
308
|
+
clip(o.detail || (cur && cur.detail) || '', 4000), Math.round(o.dueAt || (cur && cur.dueAt) || now()), clip(o.status || (cur && cur.status) || 'waiting', 20), cur ? cur.createdAt : now(), now());
|
|
309
|
+
return rowToOut(q.getOutbox.get(id));
|
|
310
|
+
}
|
|
311
|
+
const getOutbox = (id) => rowToOut(q.getOutbox.get(String(id)));
|
|
312
|
+
const outboxByStatus = (status, limit = 50) => q.outboxByStatus.all(String(status), limit).map(rowToOut);
|
|
313
|
+
|
|
314
|
+
const rowToAudit = (r) => r && { seq: Number(r.seq), at: Number(r.at), botId: r.bot_id, taskId: r.task_id, kind: r.kind, tool: r.tool, title: r.title, detail: r.detail || '', verdict: r.verdict, ok: r.ok == null ? null : !!r.ok, result: r.result || '' };
|
|
315
|
+
function audit(e) {
|
|
316
|
+
q.addAudit.run(Math.round(e.at || now()), e.botId || null, e.taskId || null, clip(e.kind || 'action', 30), e.tool ? clip(e.tool, 60) : null,
|
|
317
|
+
clip(e.title, 300), clip(e.detail, 2000), e.verdict ? clip(e.verdict, 20) : null, e.ok == null ? null : (e.ok ? 1 : 0), clip(e.result, 2000));
|
|
318
|
+
}
|
|
319
|
+
const auditSince = (since, limit = 200) => q.auditSince.all(Math.round(since), limit).map(rowToAudit);
|
|
320
|
+
const auditCount = (since, kind, botId = null) => Number(q.auditCount.get(Math.round(since), String(kind), botId, botId).n) || 0;
|
|
321
|
+
|
|
322
|
+
// ─── Housekeeping ─────────────────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
function prune(t = now()) {
|
|
325
|
+
db.prepare('DELETE FROM items WHERE source = ? AND at < ?').run('activity', t - KEEP_DAYS.activity * DAY);
|
|
326
|
+
db.prepare('DELETE FROM items WHERE source = ? AND updated_at < ?').run('screen', t - KEEP_DAYS.screen * DAY);
|
|
327
|
+
db.prepare('DELETE FROM items WHERE sensitive = 1 AND at < ?').run(t - KEEP_DAYS.sensitive * DAY);
|
|
328
|
+
db.prepare('DELETE FROM items WHERE at < ? AND (end_at IS NULL OR end_at < ?)').run(t - KEEP_DAYS.default * DAY, t - KEEP_DAYS.default * DAY);
|
|
329
|
+
db.prepare('DELETE FROM nudges WHERE at < ?').run(t - KEEP_DAYS.default * DAY);
|
|
330
|
+
db.prepare('DELETE FROM audit WHERE at < ?').run(t - KEEP_DAYS.audit * DAY);
|
|
331
|
+
db.prepare("DELETE FROM outbox WHERE status != 'waiting' AND updated_at < ?").run(t - KEEP_DAYS.default * DAY);
|
|
332
|
+
}
|
|
333
|
+
function sources() {
|
|
334
|
+
return Object.fromEntries(q.countSource.all().map((r) => [r.source, { count: Number(r.n), last: Number(r.last) || 0 }]));
|
|
335
|
+
}
|
|
336
|
+
const removeSource = (source) => q.deleteSource.run(String(source)).changes;
|
|
337
|
+
const deleteItem = (id) => q.deleteItem.run(String(id)).changes;
|
|
338
|
+
function clearAll() {
|
|
339
|
+
db.exec('DELETE FROM items; DELETE FROM people; DELETE FROM state; DELETE FROM nudges;');
|
|
340
|
+
}
|
|
341
|
+
const close = () => { try { db.close(); } catch {} };
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
kind: driver.kind, file, canSeal: !!box,
|
|
345
|
+
upsertItem, getItem, items, search, deleteItem, removeSource, sources,
|
|
346
|
+
notePerson, person, findPeople, topPeople,
|
|
347
|
+
getState, setState, deleteState,
|
|
348
|
+
addNudge, nudgeFeedback, getNudge, lastNudgeFor, nudgesSince,
|
|
349
|
+
saveTask, getTask, tasksByStatus, recentTasks,
|
|
350
|
+
saveOutbox, getOutbox, outboxByStatus,
|
|
351
|
+
audit, auditSince, auditCount,
|
|
352
|
+
prune, clearAll, close,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = { openLifeStore, sealer, defaultFile, KEEP_DAYS };
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proactive outreach: when a bot should reach out without being asked.
|
|
3
|
+
*
|
|
4
|
+
* Every minute the host (life-desktop.js) calls due() with the life store and
|
|
5
|
+
* a snapshot (life.js). Each trigger looks for its moment with no model call:
|
|
6
|
+
*
|
|
7
|
+
* leaving an event or plan with a place starts in 5 to 20 minutes
|
|
8
|
+
* meeting a meeting with people starts in 2 to 10 minutes
|
|
9
|
+
* email an important email has waited 24 hours for the user's reply
|
|
10
|
+
* deadline a deadline in the next 3 days, and no push to its project in 3 days
|
|
11
|
+
* watched a while after something watched for 10+ minutes, ask how it went
|
|
12
|
+
* brief once in the morning, the day in a few lines (off by default)
|
|
13
|
+
* activity the user has moved far less than usual for two days, or stopped
|
|
14
|
+
* working out: one gentle "everything okay?" (needs health data)
|
|
15
|
+
*
|
|
16
|
+
* What comes out is checked against the user's limits: a daily budget, quiet
|
|
17
|
+
* hours, never twice for the same thing, a gap between nudges, nothing but
|
|
18
|
+
* "leaving" while they are on a call or in a meeting, and their feedback
|
|
19
|
+
* (Not now waits an hour, Stop these turns the trigger off). Then ONE model
|
|
20
|
+
* call (judgePrompt) decides which are worth sending and words them in the
|
|
21
|
+
* bot's own voice; without a model the fallback lines go out.
|
|
22
|
+
*/
|
|
23
|
+
const L = require('./life.js');
|
|
24
|
+
const MW = require('./mail-watch.js');
|
|
25
|
+
|
|
26
|
+
const MIN = 60000; const HOUR = 3600000; const DAY = 86400000;
|
|
27
|
+
|
|
28
|
+
const TRIGGERS = {
|
|
29
|
+
leaving: { label: 'Before you head out', note: 'Before a plan or event with a place: wallet, keys, when to leave.', domain: 'life' },
|
|
30
|
+
meeting: { label: 'Before a meeting', note: 'A few minutes before a meeting: who it is with and what you last talked about.', domain: 'work' },
|
|
31
|
+
email: { label: 'Someone is waiting on you', note: 'An important email you have not answered in 24 hours.', domain: 'work' },
|
|
32
|
+
deadline: { label: 'Deadlines', note: 'A deadline in the next 3 days and no push to the project in 3 days.', domain: 'work' },
|
|
33
|
+
watched: { label: 'Follow-ups', note: 'A few hours after you watched something for 10 minutes or more, a bot asks how it went.', domain: 'life' },
|
|
34
|
+
brief: { label: 'Morning brief', note: 'Your day in a few lines, once in the morning.', domain: 'life', off: true },
|
|
35
|
+
activity: { label: 'When you move less than usual', note: 'If your steps or workouts drop a lot for a couple of days, a bot gently asks if everything is okay.', domain: 'life', topic: 'health' },
|
|
36
|
+
};
|
|
37
|
+
const PRIORITY = { leaving: 3, meeting: 2, email: 2, deadline: 2, watched: 1, brief: 1, activity: 1 };
|
|
38
|
+
const GAP_MS = 20 * MIN;
|
|
39
|
+
const DEADLINE_RE = /\b(deadline|due|launch|ship|release|submit|submission|demo|presentation|pitch|go live|hand ?in|exam)\b/i;
|
|
40
|
+
const ONLINE_RE = /^(https?:|www\.)|meet\.google|zoom\.us|teams\.microsoft|webex|online|virtual|google meet|zoom|teams call/i;
|
|
41
|
+
|
|
42
|
+
const triggerOn = (k, s) => (s && s.triggers && typeof s.triggers[k] === 'boolean' ? s.triggers[k] : !TRIGGERS[k].off);
|
|
43
|
+
const short = (s, n) => { s = String(s || '').replace(/\s+/g, ' ').trim(); return s.length > n ? `${s.slice(0, n - 1)}…` : s; };
|
|
44
|
+
const firstName = (p) => String(p || '').replace(/<.*>/, '').replace(/@.*/, '').trim().split(/\s+/)[0] || String(p || '');
|
|
45
|
+
const emailOf = (p) => ((/<([^>]+)>/.exec(String(p || '')) || [])[1] || (String(p || '').includes('@') ? String(p).trim() : '')).toLowerCase();
|
|
46
|
+
const localHour = (t, tz) => Number(L.clock(t, tz).slice(0, 2));
|
|
47
|
+
|
|
48
|
+
// ─── The triggers ───────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
function leaving({ snap, now }) {
|
|
51
|
+
// A plan someone mentioned that is also on the calendar is one event: nudge once, for the calendar entry.
|
|
52
|
+
const next = snap.next.filter((e) => !(e.source === 'plans' && snap.next.some((c) => c.source !== 'plans' && L.sameEvent(c, e))));
|
|
53
|
+
return next.filter((e) => e.place && !ONLINE_RE.test(e.place) && !e.allDay && !e.live && e.at - now >= 5 * MIN && e.at - now <= 20 * MIN).map((e) => ({
|
|
54
|
+
key: `leaving:${e.id}`, trigger: 'leaving', priority: PRIORITY.leaving, how: 'text',
|
|
55
|
+
facts: [`"${e.title}" at ${e.place} starts ${L.until(e.at - now)} (${L.clock(e.at, snap.tz)})${e.people.length ? `, with ${e.people.slice(0, 3).map(firstName).join(', ')}` : ''}.`, 'Remind them to grab what they need (wallet, keys, phone, anything the plan needs) and to leave now if it is not close.'],
|
|
56
|
+
fallback: `${short(e.title, 60)} at ${short(e.place, 40)} ${L.until(e.at - now)}. Got your wallet, keys and phone?`,
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function meeting({ snap, store, now }) {
|
|
61
|
+
return snap.next.filter((e) => e.source === 'calendar' && e.people.length && (!e.place || ONLINE_RE.test(e.place)) && !e.allDay && !e.live && e.at - now >= 2 * MIN && e.at - now <= 10 * MIN).map((e) => {
|
|
62
|
+
const facts = [`"${e.title}" starts ${L.until(e.at - now)} with ${e.people.slice(0, 4).map(firstName).join(', ')}.`];
|
|
63
|
+
for (const p of e.people.slice(0, 2)) {
|
|
64
|
+
const email = emailOf(p);
|
|
65
|
+
const last = email ? store.items({ sources: ['gmail'], text: email, limit: 1 })[0] : null;
|
|
66
|
+
if (last) facts.push(`Last email with ${firstName(p)}: "${short(last.title, 80)}" (${L.ago(now - last.at)} ago).`);
|
|
67
|
+
}
|
|
68
|
+
return { key: `meeting:${e.id}`, trigger: 'meeting', priority: PRIORITY.meeting, how: 'text', facts, fallback: `${short(e.title, 60)} ${L.until(e.at - now)} with ${e.people.slice(0, 3).map(firstName).join(', ')}.` };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function email({ store, now }) {
|
|
73
|
+
const waiting = store.getState('gmail.waiting', []) || [];
|
|
74
|
+
return waiting.filter((w) => now - w.at >= DAY && now - w.at < 7 * DAY).filter((w) => {
|
|
75
|
+
const p = store.person(w.from);
|
|
76
|
+
return w.important || (p && p.importance >= 5);
|
|
77
|
+
}).map((w) => {
|
|
78
|
+
const h = Math.round((now - w.at) / HOUR);
|
|
79
|
+
return {
|
|
80
|
+
key: `email:${w.threadId}`, trigger: 'email', priority: PRIORITY.email, how: 'text', url: w.url || '',
|
|
81
|
+
facts: [`${w.fromName || w.from} <${w.from}> emailed "${w.subject}" ${h} hours ago and the user has not replied.`, 'Offer to draft the reply.'],
|
|
82
|
+
fallback: `${w.fromName || w.from} has been waiting ${h} h for your reply to "${short(w.subject, 60)}". Want me to draft one?`,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function deadline({ store, snap, now }) {
|
|
88
|
+
const tz = snap.tz;
|
|
89
|
+
const h = localHour(now, tz);
|
|
90
|
+
if (h < 10 || h >= 20) return [];
|
|
91
|
+
const soon = store.items({ sources: ['calendar', 'plans'], from: now, to: now + 3 * DAY, limit: 60, order: 'asc' }).filter((x) => x.kind !== 'cancelled' && DEADLINE_RE.test(x.title));
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const d of soon) {
|
|
94
|
+
const words = d.title.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2);
|
|
95
|
+
const projects = snap.projects.length ? snap.projects : [];
|
|
96
|
+
const match = projects.find((p) => p.name.toLowerCase().split(/[^a-z0-9]+/).some((w) => w.length > 2 && words.includes(w))) || (projects.length === 1 ? projects[0] : null);
|
|
97
|
+
if (!match) continue;
|
|
98
|
+
const since = match.lastPush ? now - match.lastPush : Infinity;
|
|
99
|
+
if (since < 3 * DAY) continue;
|
|
100
|
+
const days = match.lastPush ? Math.floor(since / DAY) : null;
|
|
101
|
+
out.push({
|
|
102
|
+
key: `deadline:${d.id}:${L.dayKey(now, tz)}`, trigger: 'deadline', priority: PRIORITY.deadline, how: 'text',
|
|
103
|
+
facts: [`"${d.title}" is ${L.when(d.at, now, tz)}.`, `Project ${match.name}: ${days == null ? 'never pushed' : `last push ${days} days ago`}${match.unpushed ? `, ${match.unpushed} commits waiting to be pushed` : ''}${match.dirty ? ', uncommitted changes' : ''}.`],
|
|
104
|
+
fallback: `${short(d.title, 60)} is ${L.when(d.at, now, tz)}. You haven't pushed to ${match.name} ${days == null ? 'yet' : `in ${days} days`}.`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function watched({ store, now }) {
|
|
111
|
+
return store.items({ sources: ['activity'], kinds: ['watch'], from: now - 30 * HOUR, limit: 20 })
|
|
112
|
+
.filter((x) => (x.end || x.at) - x.at >= 10 * MIN && now - (x.end || x.at) >= 2 * HOUR)
|
|
113
|
+
.slice(0, 1).map((x) => ({
|
|
114
|
+
key: `watched:${x.id}`, trigger: 'watched', priority: PRIORITY.watched, how: 'text',
|
|
115
|
+
facts: [`The user watched "${x.title}"${x.data.site ? ` on ${x.data.site}` : ''} for ${Math.round(((x.end || x.at) - x.at) / MIN)} minutes, ${L.ago(now - (x.end || x.at))} ago.`, 'Ask casually what came of it or whether there is anything to follow up on. One question.'],
|
|
116
|
+
fallback: `So what happened with "${short(x.title, 70)}"? Anything worth following up on?`,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function brief({ store, snap, now, atPc }) {
|
|
121
|
+
const tz = snap.tz;
|
|
122
|
+
const h = localHour(now, tz);
|
|
123
|
+
if (h < 7 || h >= 11) return [];
|
|
124
|
+
if (!atPc && h < 9) return [];
|
|
125
|
+
const start = L.startOfDay(now, tz);
|
|
126
|
+
const today = store.items({ sources: ['calendar', 'plans'], from: start, to: start + DAY, limit: 20, order: 'asc' }).filter((x) => x.kind !== 'cancelled');
|
|
127
|
+
const waiting = (store.getState('gmail.waiting', []) || []).length;
|
|
128
|
+
const facts = [today.length ? `Today: ${today.map((x) => `${x.data && x.data.allDay ? 'all day' : L.clock(x.at, tz)} ${short(x.title, 50)}`).join('; ')}.` : 'Nothing on the calendar today.'];
|
|
129
|
+
if (waiting) facts.push(`${waiting} email${waiting === 1 ? '' : 's'} waiting on a reply.`);
|
|
130
|
+
return [{ key: `brief:${L.dayKey(now, tz)}`, trigger: 'brief', priority: PRIORITY.brief, how: 'text', facts, fallback: facts.join(' ') }];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function activity({ store, snap, now }) {
|
|
134
|
+
const h = snap.health;
|
|
135
|
+
const tz = snap.tz;
|
|
136
|
+
const hour = localHour(now, tz);
|
|
137
|
+
if (!h || hour < 9 || hour >= 21) return [];
|
|
138
|
+
// At most one every 3 days, whatever the reason.
|
|
139
|
+
if (store.nudgesSince(now - 3 * DAY, 200).some((n) => n.trigger === 'activity')) return [];
|
|
140
|
+
const facts = [];
|
|
141
|
+
let reason = '';
|
|
142
|
+
const low = (v) => v != null && h.usualSteps && v < 0.45 * h.usualSteps;
|
|
143
|
+
// The two latest days with data must both be low; today only counts once it is evening.
|
|
144
|
+
const recent = [hour >= 17 && h.stepsToday != null ? ['so far today', h.stepsToday] : null, h.stepsYesterday != null ? ['yesterday', h.stepsYesterday] : null, h.stepsDayBefore != null ? ['the day before', h.stepsDayBefore] : null].filter(Boolean).slice(0, 2);
|
|
145
|
+
if (h.usualSteps && h.stepDays >= 7 && recent.length === 2 && recent.every(([, v]) => low(v))) {
|
|
146
|
+
reason = 'steps';
|
|
147
|
+
facts.push(`Steps: ${recent.map(([when, v]) => `${v} ${when}`).join(', ')}; the user usually does about ${Math.round(h.usualSteps)} a day.`);
|
|
148
|
+
} else if (h.workoutsBefore >= 3 && h.workoutsWeek === 0) {
|
|
149
|
+
reason = 'workouts';
|
|
150
|
+
facts.push(`No workouts in the last 7 days; the week before had ${h.workoutsBefore}.`);
|
|
151
|
+
}
|
|
152
|
+
if (!reason) return [];
|
|
153
|
+
if (h.feelings.length) facts.push(`Lately they said: "${short(h.feelings[0].title, 80)}".`);
|
|
154
|
+
facts.push('Ask gently, once, whether everything is okay. No lecture, no numbers unless they help, no medical advice; offer to move or lighten things on their calendar if that helps.');
|
|
155
|
+
return [{
|
|
156
|
+
key: `activity:${reason}:${L.dayKey(now, tz)}`, trigger: 'activity', priority: PRIORITY.activity, how: 'text', facts,
|
|
157
|
+
fallback: reason === 'steps' ? 'You\'ve been moving a lot less than usual the last couple of days. Everything okay?' : 'No workouts this week, which isn\'t like you. All good?',
|
|
158
|
+
}];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const GENERATORS = { leaving, meeting, email, deadline, watched, brief, activity };
|
|
162
|
+
|
|
163
|
+
// ─── The limits ─────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* What should go out now, best first. settings: the nudges settings
|
|
167
|
+
* (on, perDay, quiet, triggers). Pure apart from reading the store.
|
|
168
|
+
*/
|
|
169
|
+
function due({ store, snap, settings = {}, now = Date.now(), atPc = false }) {
|
|
170
|
+
if (!store || !snap) return [];
|
|
171
|
+
const tz = snap.tz;
|
|
172
|
+
const all = [];
|
|
173
|
+
for (const [k, gen] of Object.entries(GENERATORS)) {
|
|
174
|
+
if (!triggerOn(k, settings)) continue;
|
|
175
|
+
try { all.push(...gen({ store, snap, now, atPc })); } catch {}
|
|
176
|
+
}
|
|
177
|
+
if (!all.length) return [];
|
|
178
|
+
const today = store.nudgesSince(L.startOfDay(now, tz), 200);
|
|
179
|
+
const perDay = Number.isFinite(Number(settings.perDay)) ? Number(settings.perDay) : 6;
|
|
180
|
+
let left = perDay - today.length;
|
|
181
|
+
let lastAt = today.length ? today[0].at : 0;
|
|
182
|
+
const quiet = settings.quiet && MW.inQuietHours(settings.quiet, new Date(now));
|
|
183
|
+
const busy = !!snap.onCall || snap.next.some((e) => e.live && e.source === 'calendar' && e.people.length && !e.allDay);
|
|
184
|
+
const skipped = store.getState('nudge.skipped', {}) || {};
|
|
185
|
+
const out = [];
|
|
186
|
+
for (const c of all.sort((a, b) => b.priority - a.priority)) {
|
|
187
|
+
const time = c.trigger === 'leaving';
|
|
188
|
+
if (left <= 0) break;
|
|
189
|
+
if ((quiet || busy) && !time) continue;
|
|
190
|
+
if (!time && now - lastAt < GAP_MS) continue;
|
|
191
|
+
if (skipped[c.key] && now - skipped[c.key] < 6 * HOUR) continue;
|
|
192
|
+
const prev = store.lastNudgeFor(c.key);
|
|
193
|
+
if (prev) {
|
|
194
|
+
// Not now: once more, an hour later. Anything else: never twice.
|
|
195
|
+
const again = prev.feedback === 'later' && now - (prev.feedbackAt || prev.at) >= HOUR && !store.getState(`nudge.again.${c.key}`);
|
|
196
|
+
if (!again) continue;
|
|
197
|
+
store.setState(`nudge.again.${c.key}`, now);
|
|
198
|
+
}
|
|
199
|
+
if (c.trigger === 'watched' && today.some((n) => n.trigger === 'watched')) continue;
|
|
200
|
+
out.push(c);
|
|
201
|
+
left--;
|
|
202
|
+
// One at a time: the gap counts from this one too (leaving is time critical and never waits).
|
|
203
|
+
if (!time) lastAt = now;
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The judge said not now: leave it for 6 hours. */
|
|
209
|
+
function skip(store, c) {
|
|
210
|
+
const s = store.getState('nudge.skipped', {}) || {};
|
|
211
|
+
s[c.key] = Date.now();
|
|
212
|
+
const keys = Object.keys(s);
|
|
213
|
+
if (keys.length > 300) for (const k of keys.slice(0, keys.length - 300)) delete s[k];
|
|
214
|
+
store.setState('nudge.skipped', s);
|
|
215
|
+
}
|
|
216
|
+
/** Not now: due() sends it once more an hour after the feedback. */
|
|
217
|
+
function later(store, nudge) { store.deleteState(`nudge.again.${nudge.key}`); }
|
|
218
|
+
|
|
219
|
+
// ─── Who says it ────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
const DOMAIN_RE = {
|
|
222
|
+
life: /\b(life|social|personal|friend|assistant|buddy|coach|health|wellbeing|planner|organi[sz]er|reminder)\b/i,
|
|
223
|
+
work: /\b(work|code|coding|dev|developer|engineer|project|email|inbox|mail|ops|deploy|manager|chief of staff)\b/i,
|
|
224
|
+
};
|
|
225
|
+
// A trigger about one subject goes to the bot that handles it before any general life bot.
|
|
226
|
+
const TOPIC_RE = {
|
|
227
|
+
health: /\b(health|fitness|workout|exercise|gym|wellbeing|wellness|sleep|steps|trainer|nutrition|diet|medicines?|doctor)\b/i,
|
|
228
|
+
};
|
|
229
|
+
/** The bot that speaks for this trigger: the user's pick, a bot that took it on, the bot for its subject, the best fit by job, the lead. */
|
|
230
|
+
function chooseBot(c, team, settings = {}) {
|
|
231
|
+
const bots = (team || []).filter((b) => b && (!b.life || b.life.read !== false));
|
|
232
|
+
if (!bots.length) return null;
|
|
233
|
+
const picked = settings.botFor && settings.botFor[c.trigger];
|
|
234
|
+
if (picked) { const b = bots.find((x) => x.id === picked); if (b) return b; }
|
|
235
|
+
const taker = bots.find((b) => b.life && (b.life.speaksFor || []).includes(c.trigger));
|
|
236
|
+
if (taker) return taker;
|
|
237
|
+
const topic = (TRIGGERS[c.trigger] || {}).topic;
|
|
238
|
+
if (topic) {
|
|
239
|
+
const own = bots.find((b) => Array.isArray(b.handles) && b.handles.includes(topic)) || bots.find((b) => TOPIC_RE[topic].test(`${b.name} ${b.specialty || ''}`));
|
|
240
|
+
if (own) return own;
|
|
241
|
+
}
|
|
242
|
+
const re = DOMAIN_RE[(TRIGGERS[c.trigger] || {}).domain];
|
|
243
|
+
const fit = re && bots.find((b) => re.test(`${b.name} ${b.specialty || ''}`));
|
|
244
|
+
if (fit) return fit;
|
|
245
|
+
return bots.find((b) => b.role === 'orchestrator') || bots[0];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ─── The judge ──────────────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
function judgePrompt(cands, snap) {
|
|
251
|
+
const toneOf = (b) => { try { return require('./bots.js').TONES[b.tone && b.tone.preset].text; } catch { return ''; } };
|
|
252
|
+
const list = cands.map((c, i) => {
|
|
253
|
+
const b = c.bot;
|
|
254
|
+
const who = b ? `${b.name}${b.specialty ? ` (${short(b.specialty, 80)})` : ''}. Tone: ${toneOf(b)}${b.tone && b.tone.custom ? ` ${short(b.tone.custom, 120)}` : ''}` : 'Codeply, the user\'s assistant';
|
|
255
|
+
return `${i + 1}. key=${c.key}\n from: ${who}\n ${c.facts.join('\n ')}`;
|
|
256
|
+
}).join('\n');
|
|
257
|
+
return `You decide whether the user's bots reach out to the user right now, without being asked, and write what they say.
|
|
258
|
+
|
|
259
|
+
${L.nowBlock(snap)}
|
|
260
|
+
|
|
261
|
+
Candidates:
|
|
262
|
+
${list}
|
|
263
|
+
|
|
264
|
+
Rules:
|
|
265
|
+
- Send only what is useful to the user right now. Skip anything that would feel like nagging or that the situation above makes pointless.
|
|
266
|
+
- Each message is one or two short sentences in that bot's own voice, like a text from a friend who has the user's back. Plain words, no markdown, no long dash, no emoji unless the tone is playful.
|
|
267
|
+
- Never mention that you watch the screen or read their email, unless the message only makes sense with it (a follow-up on something they watched is fine).
|
|
268
|
+
- how is "text", or "call" only when it is urgent and time critical.
|
|
269
|
+
|
|
270
|
+
Respond with ONLY JSON: {"messages": [{"key": "<key>", "send": true, "how": "text", "text": "..."}]}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function parseJudge(json, cands) {
|
|
274
|
+
const list = json && Array.isArray(json.messages) ? json.messages : null;
|
|
275
|
+
if (!list) return null;
|
|
276
|
+
const out = [];
|
|
277
|
+
for (const c of cands) {
|
|
278
|
+
const m = list.find((x) => x && x.key === c.key);
|
|
279
|
+
if (!m) { out.push({ key: c.key, send: true, how: c.how || 'text', text: c.fallback }); continue; }
|
|
280
|
+
const text = String(m.text || '').replace(/\s*\u2014\s*/g, ', ').replace(/\s+/g, ' ').trim().slice(0, 480);
|
|
281
|
+
out.push({ key: c.key, send: m.send !== false && !!(text || c.fallback), how: m.how === 'call' ? 'call' : 'text', text: text || c.fallback });
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports = { TRIGGERS, GENERATORS, triggerOn, due, skip, later, chooseBot, judgePrompt, parseJudge, DEADLINE_RE };
|