codeply-cli 0.5.5 → 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 -1312
- 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/life.js
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Active Context Engine: one picture of the user's life that every bot
|
|
3
|
+
* reads from (the store is lib/life-store.js, the sources lib/life-sources.js).
|
|
4
|
+
*
|
|
5
|
+
* snapshot() what matters right now: the time, what is on screen, the
|
|
6
|
+
* next events and plans, who is waiting on a reply, recent
|
|
7
|
+
* calls, projects that have gone quiet, what was watched.
|
|
8
|
+
* nowBlock() the snapshot as a short block for a bot's prompt.
|
|
9
|
+
* contextTool() the life_context tool: now, today, week, person, search,
|
|
10
|
+
* calls, email, activity, plans.
|
|
11
|
+
* notePlans() plans the bots heard about in chats, calls and email.
|
|
12
|
+
* noteHealth() steps, workouts, sleep, heart rate and how the user feels,
|
|
13
|
+
* from their phone or watch.
|
|
14
|
+
*
|
|
15
|
+
* Bots see only the sources their settings allow (bot.life, normalized in
|
|
16
|
+
* bots.js). Sensitive things (call transcripts) need life.sensitive on top.
|
|
17
|
+
*
|
|
18
|
+
* The host (Craft's life-desktop.js) opens the store and attaches it here.
|
|
19
|
+
* With nothing attached (the plain CLI) the tool is not offered.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const DAY = 86400000;
|
|
23
|
+
const HOUR = 3600000;
|
|
24
|
+
|
|
25
|
+
/** Everything the engine can know, and how the Life panel names it. */
|
|
26
|
+
const SOURCES = {
|
|
27
|
+
calendar: { label: 'Google Calendar', note: 'Your events, who is in them and where.' },
|
|
28
|
+
gmail: { label: 'Gmail, sent and received', note: 'Who wrote, what about, and who is still waiting on your reply. Headers and the preview line only.' },
|
|
29
|
+
calls: { label: 'Calls', note: 'Calls with your bots and calls the call helper heard on this PC, with a short summary.', sensitive: true },
|
|
30
|
+
projects: { label: 'Craft projects', note: 'What you are building, and when you last pushed.' },
|
|
31
|
+
activity: { label: 'What is on your screen', note: 'Which app and window you have in front, every 30 seconds. Private windows and password managers are skipped.' },
|
|
32
|
+
screen: { label: 'Text on your screen', note: 'The words in the window in front, read on this PC by the OCR built into Windows. No images are kept. Private windows and password managers are skipped.' },
|
|
33
|
+
heard: { label: 'Heard around you', note: 'Listens in the background and keeps only the important bits: to-dos, plans, names, decisions. No audio is kept.' },
|
|
34
|
+
orbit: { label: 'Orbit and the drop', note: 'What you asked them.' },
|
|
35
|
+
plans: { label: 'Plans', note: 'Plans your bots heard about in chats, calls and email.' },
|
|
36
|
+
botmail: { label: "Your bots' email", note: 'Mail to and from your bots\' own addresses.' },
|
|
37
|
+
slack: { label: 'Slack', note: 'Direct messages and mentions.' },
|
|
38
|
+
tasks: { label: 'Bot tasks', note: 'Work your bots took on by themselves.' },
|
|
39
|
+
health: { label: 'Health and activity', note: 'Steps, workouts, sleep and how you feel, from your phone or watch. Only bots you allow see it.', sensitive: true },
|
|
40
|
+
};
|
|
41
|
+
const SOURCE_KEYS = Object.keys(SOURCES);
|
|
42
|
+
|
|
43
|
+
let store = null;
|
|
44
|
+
// enabled: () => Set of the sources the user has on (null = all).
|
|
45
|
+
let hostOpts = { tz: '', userName: '', onChange: null, enabled: null };
|
|
46
|
+
|
|
47
|
+
function attach(s, opts = {}) { store = s || null; hostOpts = { ...hostOpts, ...opts }; }
|
|
48
|
+
function detach() { store = null; }
|
|
49
|
+
function available() { return !!store; }
|
|
50
|
+
function getStore() { return store; }
|
|
51
|
+
|
|
52
|
+
// ─── Bot access ─────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
function normalizeLife(l) {
|
|
55
|
+
l = l && typeof l === 'object' ? l : {};
|
|
56
|
+
const sources = Array.isArray(l.sources) ? l.sources.filter((k) => SOURCES[k]) : null;
|
|
57
|
+
return {
|
|
58
|
+
read: l.read !== false,
|
|
59
|
+
sensitive: !!l.sensitive,
|
|
60
|
+
// null = every source; a list = only these.
|
|
61
|
+
sources: sources && sources.length ? [...new Set(sources)] : null,
|
|
62
|
+
// Trigger kinds this bot speaks for (life-triggers.js), e.g. ['leaving', 'email'].
|
|
63
|
+
speaksFor: (Array.isArray(l.speaksFor) ? l.speaksFor : []).filter((k) => typeof k === 'string').slice(0, 20),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The sources this bot may read, as a Set (empty when it reads nothing). Sources the user switched off never count. */
|
|
68
|
+
function allowedFor(bot) {
|
|
69
|
+
const on = typeof hostOpts.enabled === 'function' ? hostOpts.enabled() : null;
|
|
70
|
+
const keep = (k) => !on || on.has(k);
|
|
71
|
+
if (!bot) return new Set(SOURCE_KEYS.filter((k) => !SOURCES[k].sensitive && keep(k)));
|
|
72
|
+
const l = normalizeLife(bot.life);
|
|
73
|
+
if (!l.read) return new Set();
|
|
74
|
+
const base = l.sources || SOURCE_KEYS;
|
|
75
|
+
return new Set(base.filter((k) => (!SOURCES[k].sensitive || l.sensitive) && keep(k)));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Time helpers ───────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
const tzOf = (o) => (o && o.tz) || hostOpts.tz || (() => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch { return 'UTC'; } })();
|
|
81
|
+
function fmt(t, tz, opts) {
|
|
82
|
+
try { return new Intl.DateTimeFormat('en-GB', { timeZone: tz, ...opts }).format(new Date(t)); } catch { return new Date(t).toISOString(); }
|
|
83
|
+
}
|
|
84
|
+
const clock = (t, tz) => fmt(t, tz, { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' });
|
|
85
|
+
const dayKey = (t, tz) => fmt(t, tz, { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
86
|
+
function when(t, now, tz) {
|
|
87
|
+
if (dayKey(t, tz) === dayKey(now, tz)) return clock(t, tz);
|
|
88
|
+
if (dayKey(t, tz) === dayKey(now + DAY, tz)) return `tomorrow ${clock(t, tz)}`;
|
|
89
|
+
if (dayKey(t, tz) === dayKey(now - DAY, tz)) return `yesterday ${clock(t, tz)}`;
|
|
90
|
+
return fmt(t, tz, { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' });
|
|
91
|
+
}
|
|
92
|
+
/** An all-day item's day: "today, all day", "tomorrow, all day" or "Tue 27 Oct, all day". */
|
|
93
|
+
function allDayLabel(t, now, tz) {
|
|
94
|
+
if (dayKey(t, tz) === dayKey(now, tz)) return 'today, all day';
|
|
95
|
+
if (dayKey(t, tz) === dayKey(now + DAY, tz)) return 'tomorrow, all day';
|
|
96
|
+
return `${fmt(t, tz, { weekday: 'short', day: 'numeric', month: 'short' })}, all day`;
|
|
97
|
+
}
|
|
98
|
+
// The people an item is with, minus anyone its title already names ("Meeting with Usman").
|
|
99
|
+
function others(people, title) {
|
|
100
|
+
const t = ` ${String(title || '').toLowerCase()} `;
|
|
101
|
+
return (people || []).filter((p) => { const n = firstName(p).toLowerCase(); return !n || !t.includes(` ${n} `); });
|
|
102
|
+
}
|
|
103
|
+
function ago(ms) {
|
|
104
|
+
const m = Math.round(ms / 60000);
|
|
105
|
+
if (m < 60) return `${Math.max(1, m)} min`;
|
|
106
|
+
const h = Math.round(m / 60);
|
|
107
|
+
if (h < 48) return `${h} h`;
|
|
108
|
+
return `${Math.round(h / 24)} days`;
|
|
109
|
+
}
|
|
110
|
+
function until(ms) {
|
|
111
|
+
const m = Math.round(ms / 60000);
|
|
112
|
+
if (m < 60) return `in ${Math.max(0, m)} min`;
|
|
113
|
+
const h = Math.floor(m / 60);
|
|
114
|
+
return `in ${h} h${m % 60 ? ` ${m % 60} min` : ''}`;
|
|
115
|
+
}
|
|
116
|
+
/** Start of the user's local day around t (close enough for windows of a day). */
|
|
117
|
+
function startOfDay(t, tz) {
|
|
118
|
+
const off = (() => {
|
|
119
|
+
try {
|
|
120
|
+
const p = Object.fromEntries(new Intl.DateTimeFormat('en-US', { timeZone: tz, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }).formatToParts(new Date(t)).map((x) => [x.type, x.value]));
|
|
121
|
+
return Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour % 24, +p.minute, +p.second) - Math.floor(t / 1000) * 1000;
|
|
122
|
+
} catch { return 0; }
|
|
123
|
+
})();
|
|
124
|
+
const local = t + off;
|
|
125
|
+
return local - (local % DAY) - off;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const short = (s, n) => { s = String(s || '').replace(/\s+/g, ' ').trim(); return s.length > n ? `${s.slice(0, n - 1)}…` : s; };
|
|
129
|
+
const firstName = (p) => String(p || '').replace(/<.*>/, '').replace(/@.*/, '').trim().split(/\s+/)[0] || String(p || '');
|
|
130
|
+
|
|
131
|
+
// ─── The snapshot ───────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* What matters right now, limited to the sources in `allowed` (a Set, or null for all).
|
|
135
|
+
* Plain data: the prompt block, the Life panel and the trigger engine all read it.
|
|
136
|
+
*/
|
|
137
|
+
function snapshot(o = {}) {
|
|
138
|
+
const s = o.store || store;
|
|
139
|
+
const now = o.now || Date.now();
|
|
140
|
+
const tz = tzOf(o);
|
|
141
|
+
const allowed = o.allowed || null;
|
|
142
|
+
const can = (k) => !allowed || allowed.has(k);
|
|
143
|
+
const snap = { at: now, tz, local: fmt(now, tz, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }), doing: null, onCall: null, next: [], waiting: [], calls: [], projects: [], watched: [], plans: [], tasks: [], unread: [], screen: null, heard: [], health: null };
|
|
144
|
+
if (!s) return snap;
|
|
145
|
+
|
|
146
|
+
if (can('activity')) {
|
|
147
|
+
const cur = s.getState('activity.current');
|
|
148
|
+
if (cur && now - (cur.seenAt || 0) < 3 * 60000) snap.doing = { app: cur.app, title: cur.title, kind: cur.kind || 'app', since: cur.since || cur.seenAt, minutes: Math.round((now - (cur.since || now)) / 60000) };
|
|
149
|
+
snap.watched = s.items({ sources: ['activity'], kinds: ['watch'], from: now - DAY, limit: 5 })
|
|
150
|
+
.filter((x) => (x.end || x.at) - x.at >= 3 * 60000)
|
|
151
|
+
.map((x) => ({ title: x.title, at: x.at, minutes: Math.round(((x.end || x.at) - x.at) / 60000), url: x.url }));
|
|
152
|
+
}
|
|
153
|
+
if (can('screen')) {
|
|
154
|
+
const sc = s.getState('activity.screen');
|
|
155
|
+
if (sc && sc.text && now - (sc.at || 0) < 3 * 60000) snap.screen = { app: sc.app || '', title: sc.title || '', text: short(sc.text, 600), at: sc.at };
|
|
156
|
+
}
|
|
157
|
+
if (can('heard')) {
|
|
158
|
+
snap.heard = s.items({ sources: ['heard'], from: now - 12 * HOUR, limit: 6 })
|
|
159
|
+
.map((x) => ({ id: x.id, title: x.title, at: x.at, kind: x.kind, text: short(x.body || '', 200), people: x.people }));
|
|
160
|
+
}
|
|
161
|
+
const call = s.getState('call.current');
|
|
162
|
+
if (call && now - (call.seenAt || 0) < 2 * 60000) snap.onCall = { app: call.app || '', since: call.since || call.seenAt, with: call.with || '' };
|
|
163
|
+
|
|
164
|
+
if (can('calendar') || can('plans')) {
|
|
165
|
+
const srcs = [can('calendar') && 'calendar', can('plans') && 'plans'].filter(Boolean);
|
|
166
|
+
const upcoming = s.items({ sources: srcs, from: now - 15 * 60000, to: now + 36 * HOUR, limit: 30, order: 'asc' })
|
|
167
|
+
.filter((x) => x.kind !== 'cancelled' && !(x.data && x.data.untimed) && (x.end || x.at + HOUR) > now && !(x.data && x.data.allDay && x.at < now));
|
|
168
|
+
snap.next = upcoming.slice(0, 6).map((x) => ({ id: x.id, title: x.title, at: x.at, end: x.end, place: x.place, people: x.people, source: x.source, allDay: !!(x.data && x.data.allDay), live: x.at <= now }));
|
|
169
|
+
}
|
|
170
|
+
if (can('plans')) {
|
|
171
|
+
snap.plans = s.items({ sources: ['plans'], from: now - HOUR, limit: 40, order: 'asc' })
|
|
172
|
+
.filter((x) => !x.data.done).slice(0, 6).map((x) => ({ id: x.id, title: x.title, at: x.at, place: x.place, people: x.people, timed: !x.data.untimed, allDay: !!x.data.allDay }));
|
|
173
|
+
}
|
|
174
|
+
if (can('gmail')) {
|
|
175
|
+
snap.waiting = (s.getState('gmail.waiting') || []).filter((w) => now - w.at >= 4 * HOUR).slice(0, 5)
|
|
176
|
+
.map((w) => ({ from: w.fromName || w.from, email: w.from, subject: w.subject, at: w.at, hours: Math.round((now - w.at) / HOUR), threadId: w.threadId, important: !!w.important, url: w.url }));
|
|
177
|
+
}
|
|
178
|
+
if (can('calls') || can('botmail') || can('slack')) {
|
|
179
|
+
// Calls are sensitive: a bot without that access does not get here (allowedFor).
|
|
180
|
+
if (can('calls')) {
|
|
181
|
+
snap.calls = s.items({ sources: ['calls'], from: now - 2 * DAY, limit: 3 })
|
|
182
|
+
.map((x) => ({ title: x.title, at: x.at, summary: short(x.data.summary || '', 240), people: x.people }));
|
|
183
|
+
}
|
|
184
|
+
const unread = [];
|
|
185
|
+
if (can('botmail')) for (const x of s.items({ sources: ['botmail'], kinds: ['mail_in'], from: now - 2 * DAY, limit: 5 })) if (!x.data.handled) unread.push({ via: 'email', title: x.title, from: x.people[0] || '', at: x.at });
|
|
186
|
+
if (can('slack')) for (const x of s.items({ sources: ['slack'], kinds: ['dm', 'mention'], from: now - DAY, limit: 5 })) if (!x.data.handled) unread.push({ via: 'slack', title: x.title, from: x.people[0] || '', at: x.at });
|
|
187
|
+
snap.unread = unread.slice(0, 5);
|
|
188
|
+
}
|
|
189
|
+
if (can('projects')) {
|
|
190
|
+
snap.projects = s.items({ sources: ['projects'], kinds: ['project'], limit: 30 })
|
|
191
|
+
.filter((x) => now - (x.data.lastActive || x.at) < 14 * DAY)
|
|
192
|
+
.sort((a, b) => (b.data.lastActive || b.at) - (a.data.lastActive || a.at)).slice(0, 4)
|
|
193
|
+
.map((x) => ({ name: x.title, path: x.data.path || '', lastActive: x.data.lastActive || x.at, lastPush: x.data.lastPush || null, unpushed: x.data.unpushed || 0, dirty: !!x.data.dirty, deadline: x.data.deadline || null }));
|
|
194
|
+
}
|
|
195
|
+
if (can('health')) snap.health = healthSummary(s, now, tz);
|
|
196
|
+
if (can('tasks')) {
|
|
197
|
+
snap.tasks = s.recentTasks(10).filter((t) => ['queued', 'running', 'needs_you', 'verifying'].includes(t.status)).slice(0, 4)
|
|
198
|
+
.map((t) => ({ id: t.id, text: short(t.text, 120), status: t.status, botId: t.botId }));
|
|
199
|
+
}
|
|
200
|
+
return snap;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The snapshot as a short block for a bot's system prompt. Empty when there is nothing to say. */
|
|
204
|
+
function nowBlock(snap, o = {}) {
|
|
205
|
+
if (!snap) return '';
|
|
206
|
+
const now = snap.at; const tz = snap.tz;
|
|
207
|
+
const lines = [`- ${snap.local} (${tz})`];
|
|
208
|
+
if (snap.onCall) lines.push(`- The user is on a call right now${snap.onCall.app ? ` (${snap.onCall.app})` : ''}.`);
|
|
209
|
+
if (snap.doing) lines.push(`- On screen: ${snap.doing.app}${snap.doing.title ? `, "${short(snap.doing.title, 90)}"` : ''}${snap.doing.minutes >= 2 ? ` (${snap.doing.minutes} min)` : ''}`);
|
|
210
|
+
if (snap.screen) lines.push(`- Words on screen now: "${short(snap.screen.text, 400)}"`);
|
|
211
|
+
if (snap.heard.length) lines.push(`- Heard around the user lately: ${snap.heard.slice(0, 4).map((h) => `${short(h.title, 90)} (${when(h.at, now, tz)})`).join('; ')}`);
|
|
212
|
+
if (snap.next.length) {
|
|
213
|
+
lines.push(`- Coming up: ${snap.next.slice(0, 4).map((e) => `${e.allDay ? allDayLabel(e.at, now, tz) : e.live ? 'now' : when(e.at, now, tz)} ${short(e.title, 60)}${e.place ? ` at ${short(e.place, 40)}` : ''}${others(e.people, e.title).length ? ` with ${others(e.people, e.title).slice(0, 3).map(firstName).join(', ')}` : ''}${!e.allDay && !e.live && e.at - now < 3 * HOUR ? ` (${until(e.at - now)})` : ''}`).join('; ')}`);
|
|
214
|
+
}
|
|
215
|
+
const loosePlans = snap.plans.filter((p) => !snap.next.some((e) => e.id === p.id));
|
|
216
|
+
if (loosePlans.length) lines.push(`- Plans: ${loosePlans.slice(0, 3).map((p) => `${short(p.title, 60)}${p.allDay ? ` (${allDayLabel(p.at, now, tz)})` : p.timed ? ` (${when(p.at, now, tz)})` : ''}`).join('; ')}`);
|
|
217
|
+
if (snap.waiting.length) lines.push(`- Waiting on the user's reply: ${snap.waiting.slice(0, 3).map((w) => `${w.from}, "${short(w.subject, 50)}" (${w.hours} h)`).join('; ')}`);
|
|
218
|
+
if (snap.unread.length) lines.push(`- New for the bots: ${snap.unread.slice(0, 3).map((u) => `${u.via} from ${u.from || 'someone'}: "${short(u.title, 50)}"`).join('; ')}`);
|
|
219
|
+
if (snap.calls.length) lines.push(`- Recent calls: ${snap.calls.slice(0, 2).map((c) => `${short(c.title, 50)} ${when(c.at, now, tz)}${c.summary ? `: ${short(c.summary, 120)}` : ''}`).join('; ')}`);
|
|
220
|
+
if (snap.projects.length) {
|
|
221
|
+
lines.push(`- Projects: ${snap.projects.slice(0, 3).map((p) => `${p.name}${p.lastPush ? `, last push ${ago(now - p.lastPush)} ago` : ''}${p.unpushed ? `, ${p.unpushed} commit${p.unpushed === 1 ? '' : 's'} not pushed` : ''}${p.dirty ? ', uncommitted changes' : ''}`).join('; ')}`);
|
|
222
|
+
}
|
|
223
|
+
if (snap.watched.length) lines.push(`- Watched lately: ${snap.watched.slice(0, 2).map((w) => `"${short(w.title, 60)}"`).join('; ')}`);
|
|
224
|
+
if (snap.health) lines.push(`- Health: ${healthLine(snap.health, now, tz)}`);
|
|
225
|
+
if (snap.tasks.length) lines.push(`- Bot tasks in progress: ${snap.tasks.slice(0, 3).map((t) => `${short(t.text, 60)} (${t.status.replace('_', ' ')})`).join('; ')}`);
|
|
226
|
+
if (lines.length === 1 && o.skipEmpty) return '';
|
|
227
|
+
return `RIGHT NOW (the user's life context, shared by all their bots)
|
|
228
|
+
${lines.join('\n')}
|
|
229
|
+
Use this when it helps: plan around the user's time, mention a clash, link what they ask to what is going on. Do not recite it back, and do not bring up what is on their screen unless it is relevant or they ask. For more, use life_context.`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── The life_context tool ──────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
const ABOUT = {
|
|
235
|
+
now: 'now', current: 'now', snapshot: 'now', today: 'today', day: 'today', tomorrow: 'tomorrow', week: 'week', upcoming: 'week',
|
|
236
|
+
person: 'person', people: 'person', who: 'person', contact: 'person', search: 'search', find: 'search',
|
|
237
|
+
calls: 'calls', call: 'calls', email: 'email', mail: 'email', inbox: 'email', activity: 'activity', screen: 'activity', watched: 'activity', heard: 'heard', overheard: 'heard', listened: 'heard', conversations: 'heard',
|
|
238
|
+
plans: 'plans', plan: 'plans', projects: 'projects', project: 'projects', slack: 'slack', tasks: 'tasks',
|
|
239
|
+
health: 'health', steps: 'health', workout: 'health', workouts: 'health', exercise: 'health', sleep: 'health', fitness: 'health', activity_level: 'health', heart: 'health',
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// The sources each life_context topic reads (calls and health check their own switch).
|
|
243
|
+
const ABOUT_SOURCES = {
|
|
244
|
+
today: ['calendar', 'plans', 'calls', 'gmail', 'botmail', 'slack', 'tasks'], tomorrow: ['calendar', 'plans', 'calls', 'gmail', 'botmail', 'slack', 'tasks'],
|
|
245
|
+
week: ['calendar', 'plans'], email: ['gmail', 'botmail'], activity: ['activity', 'screen', 'orbit'], heard: ['heard'],
|
|
246
|
+
plans: ['plans'], projects: ['projects'], slack: ['slack'],
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
function itemLine(x, now, tz) {
|
|
250
|
+
// A plan with only a date shows the date, never a time nobody gave.
|
|
251
|
+
// An event that ends on another day (a 48 hour hackathon) shows the end's day too.
|
|
252
|
+
const t = x.data && x.data.allDay ? allDayLabel(x.at, now, tz) : x.end && x.end - x.at > 30 * 60000 && x.source !== 'activity' ? `${when(x.at, now, tz)} to ${dayKey(x.end, tz) === dayKey(x.at, tz) ? clock(x.end, tz) : when(x.end, now, tz)}` : when(x.at, now, tz);
|
|
253
|
+
const who = x.people && x.people.length ? ` [${x.people.slice(0, 4).join(', ')}]` : '';
|
|
254
|
+
const extra = x.data && (x.data.summary || x.data.snippet) ? `: ${short(x.data.summary || x.data.snippet, 220)}` : x.body && !x.sensitive ? `: ${short(x.body, 220)}` : '';
|
|
255
|
+
return `- ${t} · ${x.source}/${x.kind} · ${short(x.title, 120)}${x.place ? ` @ ${short(x.place, 60)}` : ''}${who}${extra}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function contextTool(args, ctx = {}) {
|
|
259
|
+
if (!store) return { ok: false, output: 'The life context is only available in the Craft app.' };
|
|
260
|
+
const bot = ctx.bot || null;
|
|
261
|
+
const allowed = ctx.allowed || allowedFor(bot);
|
|
262
|
+
if (!allowed.size) return { ok: false, output: 'This bot is not allowed to read the user\'s life context. The user can turn it on in Crew, Life.' };
|
|
263
|
+
const about = ABOUT[String(args.about || 'now').trim().toLowerCase()] || 'search';
|
|
264
|
+
const query = String(args.query || args.about || '').trim();
|
|
265
|
+
const now = ctx.now || Date.now();
|
|
266
|
+
const tz = tzOf(ctx);
|
|
267
|
+
const srcs = [...allowed];
|
|
268
|
+
const lines = (list) => (list.length ? list.map((x) => itemLine(x, now, tz)).join('\n') : '(nothing)');
|
|
269
|
+
// Never an empty list: the store reads an empty source list as "every source", sensitive ones included.
|
|
270
|
+
const pick = (want) => { const ok = want.filter((k) => allowed.has(k)); return ok.length ? ok : ['(none allowed)']; };
|
|
271
|
+
const needs = ABOUT_SOURCES[about];
|
|
272
|
+
if (needs && !needs.some((k) => allowed.has(k))) return { ok: false, output: `This bot is not allowed to read ${about === 'heard' ? 'what is heard around the user' : about}. The user can allow it in the bot's Life settings.` };
|
|
273
|
+
let out = '';
|
|
274
|
+
if (about === 'now') out = nowBlock(snapshot({ now, tz, allowed })) || 'Nothing known right now.';
|
|
275
|
+
else if (about === 'today' || about === 'tomorrow') {
|
|
276
|
+
const start = startOfDay(now, tz) + (about === 'tomorrow' ? DAY : 0);
|
|
277
|
+
out = `${about === 'today' ? 'Today' : 'Tomorrow'}:\n${lines(store.items({ sources: pick(['calendar', 'plans', 'calls', 'gmail', 'botmail', 'slack', 'tasks']), from: start, to: start + DAY, limit: 60, order: 'asc' }).filter((x) => x.kind !== 'cancelled'))}`;
|
|
278
|
+
} else if (about === 'week') out = `The next 7 days:\n${lines(store.items({ sources: pick(['calendar', 'plans']), from: now, to: now + 7 * DAY, limit: 60, order: 'asc' }).filter((x) => x.kind !== 'cancelled'))}`;
|
|
279
|
+
else if (about === 'person') {
|
|
280
|
+
if (!query) return { ok: false, output: 'life_context about=person needs <query>: a name or an email address.' };
|
|
281
|
+
const people = store.findPeople(query, 3);
|
|
282
|
+
const hits = store.search(query, { limit: 15, sources: srcs });
|
|
283
|
+
out = `${people.length ? people.map((p) => `${p.name || p.emails[0]} <${p.emails.join(', ')}>, importance ${p.importance}/10${p.lastFrom ? `, last wrote ${ago(now - p.lastFrom)} ago` : ''}${p.lastTo ? `, you last wrote ${ago(now - p.lastTo)} ago` : ''}${p.note ? `. ${p.note}` : ''}`).join('\n') : `No contact card for "${query}".`}\nRecent:\n${lines(hits)}`;
|
|
284
|
+
} else if (about === 'calls') {
|
|
285
|
+
if (!allowed.has('calls')) return { ok: false, output: 'This bot is not allowed to read calls. The user can allow it in the bot\'s Life settings.' };
|
|
286
|
+
out = `Recent calls:\n${lines(store.items({ sources: ['calls'], from: now - 14 * DAY, limit: 10 }))}`;
|
|
287
|
+
} else if (about === 'email') {
|
|
288
|
+
const waiting = (store.getState('gmail.waiting') || []).slice(0, 8);
|
|
289
|
+
out = `${waiting.length ? `Waiting on the user's reply:\n${waiting.map((w) => `- ${w.fromName || w.from} <${w.from}>: "${short(w.subject, 80)}", ${ago(now - w.at)} ago${w.important ? ' (important)' : ''}`).join('\n')}\n` : ''}Recent email:\n${lines(store.items({ sources: pick(['gmail', 'botmail']), from: now - 3 * DAY, limit: 15 }))}`;
|
|
290
|
+
} else if (about === 'activity') out = `On screen, the last few hours:\n${lines(store.items({ sources: pick(['activity', 'screen', 'orbit']), from: now - 6 * HOUR, limit: 25 }))}`;
|
|
291
|
+
else if (about === 'heard') out = `Heard around the user (the important bits only):
|
|
292
|
+
${lines(store.items({ sources: pick(['heard']), from: now - 3 * DAY, limit: 25 }))}`;
|
|
293
|
+
else if (about === 'plans') out = `Plans:\n${lines(store.items({ sources: pick(['plans']), from: now - DAY, limit: 20, order: 'asc' }))}`;
|
|
294
|
+
else if (about === 'projects') out = `Projects:\n${lines(store.items({ sources: pick(['projects']), limit: 10 }))}`;
|
|
295
|
+
else if (about === 'slack') out = `Slack:\n${lines(store.items({ sources: pick(['slack']), from: now - 3 * DAY, limit: 20 }))}`;
|
|
296
|
+
else if (about === 'health') {
|
|
297
|
+
if (!allowed.has('health')) return { ok: false, output: 'This bot is not allowed to read health data. The user can allow it in the bot\'s Life settings (Sensitive things).' };
|
|
298
|
+
out = healthReport(store, now, tz);
|
|
299
|
+
} else if (about === 'tasks') out = `Bot tasks:\n${store.recentTasks(10).map((t) => `- ${t.status}: ${short(t.text, 140)}`).join('\n') || '(nothing)'}`;
|
|
300
|
+
else {
|
|
301
|
+
if (!query) return { ok: false, output: 'life_context needs <about> (now, today, tomorrow, week, person, calls, email, activity, heard, plans, projects, slack, tasks) or <query> to search.' };
|
|
302
|
+
out = `Search "${query}":\n${lines(store.search(query, { limit: 15, sources: srcs }))}`;
|
|
303
|
+
}
|
|
304
|
+
return { ok: true, output: out.slice(0, 8000), meta: { label: `${about}${query && about !== 'now' ? `: ${short(query, 40)}` : ''}` } };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ─── Plans ──────────────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
/** "2026-10-17T20:00" in the user's zone to epoch ms (or NaN). */
|
|
310
|
+
function localIso(s, tz) {
|
|
311
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2}))?/.exec(String(s || '').trim());
|
|
312
|
+
if (!m) { const d = Date.parse(s); return Number.isNaN(d) ? NaN : d; }
|
|
313
|
+
const guess = Date.UTC(+m[1], +m[2] - 1, +m[3], +(m[4] || 12), +(m[5] || 0));
|
|
314
|
+
const off = (t) => {
|
|
315
|
+
try {
|
|
316
|
+
const p = Object.fromEntries(new Intl.DateTimeFormat('en-US', { timeZone: tz, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).formatToParts(new Date(t)).map((x) => [x.type, x.value]));
|
|
317
|
+
return Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour % 24, +p.minute) - Math.floor(t / 60000) * 60000;
|
|
318
|
+
} catch { return 0; }
|
|
319
|
+
};
|
|
320
|
+
return guess - off(guess - off(guess));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Words too common to tell two events apart.
|
|
324
|
+
const PLAIN_WORDS = new Set(['with', 'the', 'and', 'for', 'from', 'meeting', 'call', 'event', 'plan', 'plans', 'session', 'time', 'today', 'tomorrow', 'tonight', 'going', 'our', 'his', 'her', 'their']);
|
|
325
|
+
function titleWords(s) {
|
|
326
|
+
return new Set(String(s || '').toLowerCase().replace(/[^\p{L}\p{N} ]+/gu, ' ').split(/\s+/).filter((w) => w.length >= 3 && !PLAIN_WORDS.has(w)));
|
|
327
|
+
}
|
|
328
|
+
/** Two items are the same event: they start within 3 hours and their titles share a main word. */
|
|
329
|
+
function sameEvent(a, b) {
|
|
330
|
+
if (!a || !b || !Number.isFinite(a.at) || !Number.isFinite(b.at) || Math.abs(a.at - b.at) > 3 * HOUR) return false;
|
|
331
|
+
const wa = titleWords(a.title);
|
|
332
|
+
for (const w of titleWords(b.title)) if (wa.has(w)) return true;
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Plans the bots heard about: [{ what, when, where, who }] from learnFromTurn
|
|
338
|
+
* or a call summary. A plan without a time is kept for a week as "untimed".
|
|
339
|
+
*/
|
|
340
|
+
function notePlans(plans, o = {}) {
|
|
341
|
+
if (!store || !Array.isArray(plans)) return [];
|
|
342
|
+
const tz = tzOf(o);
|
|
343
|
+
const now = o.now || Date.now();
|
|
344
|
+
const out = [];
|
|
345
|
+
for (const p of plans.slice(0, 5)) {
|
|
346
|
+
const what = short(p && p.what, 140);
|
|
347
|
+
if (!what || what.length < 3) continue;
|
|
348
|
+
let at = p.when ? localIso(p.when, tz) : NaN;
|
|
349
|
+
const dateOnly = /^\s*\d{4}-\d{2}-\d{2}\s*$/.test(String(p.when || ''));
|
|
350
|
+
const untimed = !Number.isFinite(at);
|
|
351
|
+
if (untimed) at = now;
|
|
352
|
+
if (!untimed && at < now - DAY) continue;
|
|
353
|
+
// Already on the calendar: a second copy means two "leave now" nudges for one match.
|
|
354
|
+
if (!untimed && store.items({ sources: ['calendar'], from: at - 3 * HOUR, to: at + 3 * HOUR, limit: 20 }).some((c) => c.kind !== 'cancelled' && sameEvent(c, { title: what, at }))) continue;
|
|
355
|
+
const key = what.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().slice(0, 60);
|
|
356
|
+
const id = `plans:${dayKey(at, tz)}:${key}`;
|
|
357
|
+
const who = (Array.isArray(p.who) ? p.who : p.who ? [p.who] : []).map((x) => short(x, 60)).filter(Boolean).slice(0, 6);
|
|
358
|
+
out.push(store.upsertItem({ id, source: 'plans', kind: 'plan', at, end: untimed ? now + 7 * DAY : null, title: what, place: short(p.where, 120), people: who, data: { untimed, ...(dateOnly ? { allDay: true } : {}), from: o.source || 'chat', botId: o.botId || null } }));
|
|
359
|
+
}
|
|
360
|
+
if (out.length && hostOpts.onChange) { try { hostOpts.onChange('plans'); } catch {} }
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** The part of learnFromTurn's prompt that asks for plans (bots.js). */
|
|
365
|
+
function plansAsk(now = Date.now(), tz = tzOf()) {
|
|
366
|
+
return `Also list plans or commitments with a time or a place that the user mentioned (meeting someone, an appointment, a trip, a deadline): {"what": "Dinner with Ali", "when": "YYYY-MM-DDTHH:MM in the user's local time, YYYY-MM-DD for a day with no time, or empty", "where": "", "who": ["Ali"]}. It is ${fmt(now, tz, { weekday: 'long', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })} for the user now (${tz}). Most exchanges have none: use an empty list.`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ─── Health ─────────────────────────────────────────────────────────────────
|
|
370
|
+
// Samples from the user's phone or watch (the phone app sends them through
|
|
371
|
+
// the desktop bridge). Steps are one item per day (the day's total so far),
|
|
372
|
+
// workouts and feelings one item each, sleep one item per night.
|
|
373
|
+
|
|
374
|
+
const HEALTH_KINDS = new Set(['steps', 'workout', 'sleep', 'heart', 'feeling']);
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* samples: [{ kind: 'steps'|'workout'|'sleep'|'heart'|'feeling', at, end?, value?, unit?, title?, note? }]
|
|
378
|
+
* steps value = the day's total so far (at: any time that day)
|
|
379
|
+
* workout title ("Walk", "Gym"), at and end, value = active calories (optional)
|
|
380
|
+
* sleep at = asleep, end = awake
|
|
381
|
+
* heart value = resting heart rate (bpm) for the day
|
|
382
|
+
* feeling title or note: how the user said they feel ("ankle hurts", "great")
|
|
383
|
+
*/
|
|
384
|
+
function noteHealth(samples, o = {}) {
|
|
385
|
+
if (!store || !Array.isArray(samples)) return [];
|
|
386
|
+
const tz = tzOf(o);
|
|
387
|
+
const out = [];
|
|
388
|
+
for (const raw of samples.slice(0, 500)) {
|
|
389
|
+
if (!raw || !HEALTH_KINDS.has(raw.kind)) continue;
|
|
390
|
+
const at = Math.round(Number(raw.at) || Date.now());
|
|
391
|
+
const day = dayKey(at, tz);
|
|
392
|
+
const value = Number(raw.value);
|
|
393
|
+
const base = { source: 'health', kind: raw.kind, at, sensitive: true };
|
|
394
|
+
let item = null;
|
|
395
|
+
if (raw.kind === 'steps' && Number.isFinite(value)) item = { ...base, id: `health:steps:${day}`, at: startOfDay(at, tz) + 12 * HOUR, title: `${Math.round(value)} steps`, data: { value: Math.round(value), day } };
|
|
396
|
+
else if (raw.kind === 'heart' && Number.isFinite(value)) item = { ...base, id: `health:heart:${day}`, title: `Resting heart rate ${Math.round(value)} bpm`, data: { value: Math.round(value), day } };
|
|
397
|
+
else if (raw.kind === 'sleep' && raw.end) item = { ...base, id: `health:sleep:${dayKey(Number(raw.end), tz)}`, end: Math.round(Number(raw.end)), title: `Slept ${((Number(raw.end) - at) / HOUR).toFixed(1)} h`, data: { hours: Math.round(((Number(raw.end) - at) / HOUR) * 10) / 10 } };
|
|
398
|
+
else if (raw.kind === 'workout') item = { ...base, id: `health:workout:${at}`, end: raw.end ? Math.round(Number(raw.end)) : null, title: short(raw.title || 'Workout', 80), data: { minutes: raw.end ? Math.round((Number(raw.end) - at) / 60000) : Number(raw.minutes) || 0, value: Number.isFinite(value) ? value : null } };
|
|
399
|
+
else if (raw.kind === 'feeling') item = { ...base, id: `health:feeling:${at}`, title: short(raw.title || raw.note || 'How they feel', 160), body: short(raw.note || '', 600), data: {} };
|
|
400
|
+
if (item) { try { out.push(store.upsertItem(item)); } catch {} }
|
|
401
|
+
}
|
|
402
|
+
if (out.length && hostOpts.onChange) { try { hostOpts.onChange('health'); } catch {} }
|
|
403
|
+
return out;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const median = (xs) => { const a = xs.slice().sort((x, y) => x - y); return a.length ? a[Math.floor(a.length / 2)] : 0; };
|
|
407
|
+
|
|
408
|
+
/** Today's steps, the usual day (median of the 14 before), workouts and sleep. Null with no data at all. */
|
|
409
|
+
function healthSummary(s, now, tz) {
|
|
410
|
+
const start = startOfDay(now, tz);
|
|
411
|
+
const items = s.items({ sources: ['health'], from: start - 15 * DAY, limit: 300 });
|
|
412
|
+
if (!items.length) return null;
|
|
413
|
+
const steps = items.filter((x) => x.kind === 'steps');
|
|
414
|
+
const today = steps.find((x) => x.data.day === dayKey(now, tz));
|
|
415
|
+
const yesterday = steps.find((x) => x.data.day === dayKey(now - DAY, tz));
|
|
416
|
+
const dayBefore = steps.find((x) => x.data.day === dayKey(now - 2 * DAY, tz));
|
|
417
|
+
const before = steps.filter((x) => x.at < start).map((x) => x.data.value);
|
|
418
|
+
const workouts = items.filter((x) => x.kind === 'workout').sort((a, b) => b.at - a.at);
|
|
419
|
+
const sleep = items.filter((x) => x.kind === 'sleep').sort((a, b) => b.at - a.at)[0];
|
|
420
|
+
const heart = items.filter((x) => x.kind === 'heart').sort((a, b) => b.at - a.at);
|
|
421
|
+
const feelings = items.filter((x) => x.kind === 'feeling' && now - x.at < 3 * DAY).sort((a, b) => b.at - a.at);
|
|
422
|
+
return {
|
|
423
|
+
stepsToday: today ? today.data.value : null, stepsYesterday: yesterday ? yesterday.data.value : null, stepsDayBefore: dayBefore ? dayBefore.data.value : null,
|
|
424
|
+
usualSteps: before.length >= 5 ? median(before) : null, stepDays: before.length,
|
|
425
|
+
lastWorkout: workouts[0] ? { title: workouts[0].title, at: workouts[0].at, minutes: workouts[0].data.minutes } : null,
|
|
426
|
+
workoutsWeek: workouts.filter((x) => now - x.at < 7 * DAY).length,
|
|
427
|
+
workoutsBefore: workouts.filter((x) => now - x.at >= 7 * DAY && now - x.at < 14 * DAY).length,
|
|
428
|
+
sleepHours: sleep && now - (sleep.end || sleep.at) < 36 * HOUR ? sleep.data.hours : null,
|
|
429
|
+
restingHeart: heart[0] ? heart[0].data.value : null, usualHeart: heart.length >= 5 ? median(heart.slice(1).map((x) => x.data.value)) : null,
|
|
430
|
+
feelings: feelings.slice(0, 3).map((x) => ({ title: x.title, at: x.at })),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function healthLine(h, now, tz) {
|
|
435
|
+
const parts = [];
|
|
436
|
+
if (h.stepsToday != null) parts.push(`${h.stepsToday.toLocaleString('en-US')} steps today${h.usualSteps ? ` (usually about ${Math.round(h.usualSteps / 100) * 100} a day)` : ''}`);
|
|
437
|
+
else if (h.stepsYesterday != null) parts.push(`${h.stepsYesterday.toLocaleString('en-US')} steps yesterday`);
|
|
438
|
+
if (h.lastWorkout) parts.push(`last workout ${short(h.lastWorkout.title, 30)}${h.lastWorkout.minutes ? ` ${h.lastWorkout.minutes} min` : ''} ${when(h.lastWorkout.at, now, tz)}`);
|
|
439
|
+
if (h.sleepHours != null) parts.push(`slept ${h.sleepHours} h`);
|
|
440
|
+
if (h.restingHeart && h.usualHeart && Math.abs(h.restingHeart - h.usualHeart) >= 6) parts.push(`resting heart rate ${h.restingHeart} (usually ${h.usualHeart})`);
|
|
441
|
+
if (h.feelings.length) parts.push(`said: "${short(h.feelings[0].title, 60)}" (${when(h.feelings[0].at, now, tz)})`);
|
|
442
|
+
return parts.join('; ') || 'nothing recent';
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function healthReport(s, now, tz) {
|
|
446
|
+
const start = startOfDay(now, tz);
|
|
447
|
+
const items = s.items({ sources: ['health'], from: start - 14 * DAY, limit: 300, order: 'asc' });
|
|
448
|
+
if (!items.length) return 'No health data yet. It comes from the user\'s phone or watch.';
|
|
449
|
+
const h = healthSummary(s, now, tz);
|
|
450
|
+
const steps = items.filter((x) => x.kind === 'steps').map((x) => `${x.data.day.slice(0, 5)} ${x.data.value}`).join(', ');
|
|
451
|
+
const rest = items.filter((x) => x.kind !== 'steps').slice(-25).map((x) => `- ${when(x.at, now, tz)} · ${x.kind} · ${x.title}${x.data && x.data.minutes ? ` (${x.data.minutes} min)` : ''}`).join('\n');
|
|
452
|
+
return `Health, last 14 days (this is private: use it to look after the user, never to lecture or share).\nNow: ${healthLine(h, now, tz)}\nDaily steps: ${steps || '(none)'}\n${rest}`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* The user (or a bot for them) emailed these addresses: they are no longer
|
|
457
|
+
* waiting on a reply. addresses: "a@x.com, Name <b@y.com>" or a list.
|
|
458
|
+
*/
|
|
459
|
+
function noteSent(addresses) {
|
|
460
|
+
if (!store) return 0;
|
|
461
|
+
const list = (Array.isArray(addresses) ? addresses : String(addresses || '').split(/[,;]/))
|
|
462
|
+
.map((a) => ((/<([^>]+)>/.exec(String(a)) || [])[1] || String(a)).trim().toLowerCase()).filter((a) => a.includes('@'));
|
|
463
|
+
if (!list.length) return 0;
|
|
464
|
+
// Remembered on the contact too, so an email from them that the sync only
|
|
465
|
+
// reads after this reply does not put them back on the waiting list.
|
|
466
|
+
for (const a of list) { try { store.notePerson({ email: a, lastTo: Date.now() }); } catch {} }
|
|
467
|
+
const sent = new Set(list);
|
|
468
|
+
const waiting = store.getState('gmail.waiting') || [];
|
|
469
|
+
const left = waiting.filter((w) => !sent.has(String(w.from || '').trim().toLowerCase()));
|
|
470
|
+
if (left.length === waiting.length) return 0;
|
|
471
|
+
store.setState('gmail.waiting', left);
|
|
472
|
+
if (hostOpts.onChange) { try { hostOpts.onChange('gmail'); } catch {} }
|
|
473
|
+
return waiting.length - left.length;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Something happened that the bots should know (a call ended, an Orbit question): one store write. */
|
|
477
|
+
function note(item) {
|
|
478
|
+
if (!store || !item) return null;
|
|
479
|
+
try { const r = store.upsertItem(item); if (hostOpts.onChange) hostOpts.onChange(item.source); return r; } catch { return null; }
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
module.exports = {
|
|
483
|
+
SOURCES, SOURCE_KEYS, attach, detach, available, getStore,
|
|
484
|
+
normalizeLife, allowedFor, snapshot, nowBlock, contextTool, notePlans, plansAsk, note, noteSent, noteHealth, healthSummary, sameEvent,
|
|
485
|
+
// for tests and the trigger engine
|
|
486
|
+
when, ago, until, clock, dayKey, startOfDay, localIso, tzOf,
|
|
487
|
+
};
|
package/lib/mail-watch.js
CHANGED
|
@@ -285,11 +285,11 @@ function gmailApi({ token, base = GMAIL_BASE, fetchImpl = (...a) => fetch(...a)
|
|
|
285
285
|
const qs = (o) => new URLSearchParams(Object.entries(o).filter(([, v]) => v !== undefined && v !== '')).toString();
|
|
286
286
|
return {
|
|
287
287
|
profile: () => req('GET', '/profile'),
|
|
288
|
-
/** New inbox message ids since startHistoryId, and the newest historyId. */
|
|
289
|
-
async history(startHistoryId) {
|
|
288
|
+
/** New inbox message ids since startHistoryId, and the newest historyId. labelId null: every label (the life context reads SENT too). */
|
|
289
|
+
async history(startHistoryId, labelId = 'INBOX') {
|
|
290
290
|
const ids = []; let pageToken = ''; let historyId = String(startHistoryId);
|
|
291
291
|
for (let page = 0; page < 5; page++) {
|
|
292
|
-
const r = await req('GET', `/history?${qs({ startHistoryId, historyTypes: 'messageAdded', labelId:
|
|
292
|
+
const r = await req('GET', `/history?${qs({ startHistoryId, historyTypes: 'messageAdded', labelId: labelId || undefined, maxResults: 100, pageToken })}`);
|
|
293
293
|
for (const h of r.history || []) for (const a of h.messagesAdded || []) if (a.message && a.message.id) ids.push(a.message.id);
|
|
294
294
|
if (r.historyId) historyId = String(r.historyId);
|
|
295
295
|
if (!r.nextPageToken) break;
|