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.
Files changed (429) hide show
  1. package/LICENSE +21 -21
  2. package/THIRD_PARTY_NOTICES.md +65 -65
  3. package/bin/codeply.js +8 -3
  4. package/lib/agent.mjs +2596 -2268
  5. package/lib/ai.js +135 -27
  6. package/lib/ai.mjs +6 -6
  7. package/lib/apply-patch.js +217 -217
  8. package/lib/arity-table.json +138 -138
  9. package/lib/arity.js +70 -70
  10. package/lib/auth.mjs +6 -6
  11. package/lib/autopilot.js +84 -0
  12. package/lib/bot-mail.js +46 -0
  13. package/lib/bots.js +142 -15
  14. package/lib/brain.js +914 -0
  15. package/lib/call-assist.js +120 -0
  16. package/lib/chatgpt.js +282 -282
  17. package/lib/cloud-browser.mjs +143 -143
  18. package/lib/cloud.mjs +1396 -1312
  19. package/lib/commands.js +103 -103
  20. package/lib/computer.js +136 -0
  21. package/lib/design-library/assemble-artifact.mjs +21 -21
  22. package/lib/design-library/cleanup.mjs +45 -45
  23. package/lib/design-library/index.json +22379 -22379
  24. package/lib/design-library/make-table-data.mjs +39 -39
  25. package/lib/design-tools.mjs +173 -0
  26. package/lib/diagnostics.js +192 -192
  27. package/lib/dsh-acp.mjs +444 -0
  28. package/lib/edit-engine.js +364 -364
  29. package/lib/github-agent.mjs +326 -326
  30. package/lib/handoff.mjs +245 -0
  31. package/lib/history.js +78 -75
  32. package/lib/life-sources.js +319 -0
  33. package/lib/life-store.js +356 -0
  34. package/lib/life-triggers.js +286 -0
  35. package/lib/life.js +487 -0
  36. package/lib/mail-watch.js +3 -3
  37. package/lib/mcp.js +258 -258
  38. package/lib/native-tools.mjs +17 -5
  39. package/lib/oauth-connectors.js +76 -21
  40. package/lib/permissions.js +109 -109
  41. package/lib/plugins.js +356 -353
  42. package/lib/reach.js +67 -67
  43. package/lib/render-md.mjs +647 -617
  44. package/lib/screen.js +77 -0
  45. package/lib/server.mjs +458 -457
  46. package/lib/session-db.js +220 -220
  47. package/lib/share.js +208 -168
  48. package/lib/skills.js +4 -1
  49. package/lib/snapshot.js +146 -146
  50. package/lib/subagents.js +4 -2
  51. package/lib/telemetry.js +105 -105
  52. package/lib/terminal-hints.js +154 -154
  53. package/lib/textarea.mjs +264 -258
  54. package/lib/tools.mjs +326 -40
  55. package/lib/tui.mjs +1459 -1086
  56. package/lib/typecheck-worker.js +147 -147
  57. package/lib/web-tools.js +209 -209
  58. package/package.json +1 -1
  59. package/skills/SOURCE.md +58 -58
  60. package/skills/accessibility/SKILL.md +147 -147
  61. package/skills/agent-architecture-audit/SKILL.md +257 -257
  62. package/skills/agent-eval/SKILL.md +147 -147
  63. package/skills/agent-harness-construction/SKILL.md +74 -74
  64. package/skills/agent-introspection-debugging/SKILL.md +154 -154
  65. package/skills/agent-payment-x402/SKILL.md +225 -225
  66. package/skills/agent-self-evaluation/SKILL.md +182 -182
  67. package/skills/agent-self-evaluation/references/evaluation-criteria.md +71 -71
  68. package/skills/agent-self-evaluation/references/hook-integration.md +64 -64
  69. package/skills/agent-sort/SKILL.md +216 -216
  70. package/skills/agentic-engineering/SKILL.md +64 -64
  71. package/skills/agentic-os/SKILL.md +388 -388
  72. package/skills/ai-first-engineering/SKILL.md +52 -52
  73. package/skills/ai-regression-testing/SKILL.md +386 -386
  74. package/skills/android-clean-architecture/SKILL.md +340 -340
  75. package/skills/angular-developer/SKILL.md +155 -155
  76. package/skills/angular-developer/references/angular-animations.md +160 -160
  77. package/skills/angular-developer/references/angular-aria.md +410 -410
  78. package/skills/angular-developer/references/cli.md +86 -86
  79. package/skills/angular-developer/references/component-harnesses.md +59 -59
  80. package/skills/angular-developer/references/component-styling.md +91 -91
  81. package/skills/angular-developer/references/components.md +117 -117
  82. package/skills/angular-developer/references/creating-services.md +97 -97
  83. package/skills/angular-developer/references/data-resolvers.md +69 -69
  84. package/skills/angular-developer/references/define-routes.md +67 -67
  85. package/skills/angular-developer/references/defining-providers.md +72 -72
  86. package/skills/angular-developer/references/di-fundamentals.md +120 -120
  87. package/skills/angular-developer/references/e2e-testing.md +56 -56
  88. package/skills/angular-developer/references/effects.md +83 -83
  89. package/skills/angular-developer/references/hierarchical-injectors.md +43 -43
  90. package/skills/angular-developer/references/host-elements.md +80 -80
  91. package/skills/angular-developer/references/injection-context.md +63 -63
  92. package/skills/angular-developer/references/inputs.md +101 -101
  93. package/skills/angular-developer/references/linked-signal.md +59 -59
  94. package/skills/angular-developer/references/loading-strategies.md +61 -61
  95. package/skills/angular-developer/references/mcp.md +108 -108
  96. package/skills/angular-developer/references/navigate-to-routes.md +69 -69
  97. package/skills/angular-developer/references/outputs.md +86 -86
  98. package/skills/angular-developer/references/reactive-forms.md +122 -122
  99. package/skills/angular-developer/references/rendering-strategies.md +44 -44
  100. package/skills/angular-developer/references/resource.md +77 -77
  101. package/skills/angular-developer/references/route-animations.md +56 -56
  102. package/skills/angular-developer/references/route-guards.md +52 -52
  103. package/skills/angular-developer/references/router-lifecycle.md +45 -45
  104. package/skills/angular-developer/references/router-testing.md +87 -87
  105. package/skills/angular-developer/references/show-routes-with-outlets.md +68 -68
  106. package/skills/angular-developer/references/signal-forms.md +795 -795
  107. package/skills/angular-developer/references/signals-overview.md +94 -94
  108. package/skills/angular-developer/references/tailwind-css.md +69 -69
  109. package/skills/angular-developer/references/template-driven-forms.md +114 -114
  110. package/skills/angular-developer/references/testing-fundamentals.md +65 -65
  111. package/skills/api-connector-builder/SKILL.md +121 -121
  112. package/skills/api-design/SKILL.md +524 -524
  113. package/skills/architecture-decision-records/SKILL.md +180 -180
  114. package/skills/article-writing/SKILL.md +80 -80
  115. package/skills/automation-audit-ops/SKILL.md +143 -143
  116. package/skills/autonomous-agent-harness/SKILL.md +274 -274
  117. package/skills/autonomous-loops/SKILL.md +611 -611
  118. package/skills/backend-patterns/SKILL.md +562 -562
  119. package/skills/benchmark/SKILL.md +95 -95
  120. package/skills/benchmark-methodology/SKILL.md +191 -191
  121. package/skills/benchmark-optimization-loop/SKILL.md +71 -71
  122. package/skills/blender-motion-state-inspection/SKILL.md +165 -165
  123. package/skills/blueprint/SKILL.md +106 -106
  124. package/skills/brand-discovery/SKILL.md +145 -145
  125. package/skills/brand-discovery/references/10_purpose-why.md +40 -40
  126. package/skills/brand-discovery/references/20_positioning.md +44 -44
  127. package/skills/brand-discovery/references/30_audience-niche.md +52 -52
  128. package/skills/brand-discovery/references/40_personality-archetype.md +57 -57
  129. package/skills/brand-discovery/references/50_voice-tone.md +59 -59
  130. package/skills/brand-discovery/references/60_narrative-story.md +50 -50
  131. package/skills/brand-discovery/references/70_founder-tension.md +49 -49
  132. package/skills/brand-discovery/references/90_SYNTHESIS.md +133 -133
  133. package/skills/brand-voice/SKILL.md +98 -98
  134. package/skills/brand-voice/references/voice-profile-schema.md +55 -55
  135. package/skills/browser-qa/SKILL.md +105 -105
  136. package/skills/bun-runtime/SKILL.md +85 -85
  137. package/skills/canary-watch/SKILL.md +108 -108
  138. package/skills/carrier-relationship-management/SKILL.md +212 -212
  139. package/skills/cisco-ios-patterns/SKILL.md +164 -164
  140. package/skills/ck/SKILL.md +148 -148
  141. package/skills/claude-devfleet/SKILL.md +112 -112
  142. package/skills/click-path-audit/SKILL.md +245 -245
  143. package/skills/clickhouse-io/SKILL.md +445 -445
  144. package/skills/code-tour/SKILL.md +254 -254
  145. package/skills/codebase-onboarding/SKILL.md +234 -234
  146. package/skills/codehealth-mcp/SKILL.md +167 -167
  147. package/skills/codeply-taste/LICENSE +21 -0
  148. package/skills/codeply-taste/SKILL.md +100 -0
  149. package/skills/codeply-taste/SOURCE.md +10 -0
  150. package/skills/codeply-taste/assets/base.css +100 -0
  151. package/skills/codeply-taste/assets/base.js +37 -0
  152. package/skills/codeply-taste/library/directions.json +1078 -0
  153. package/skills/codeply-taste/library/heroes.json +894 -0
  154. package/skills/codeply-taste/library/motions.json +57 -0
  155. package/skills/codeply-taste/library/palettes.json +1485 -0
  156. package/skills/codeply-taste/library/photos.json +7240 -0
  157. package/skills/codeply-taste/library/sections.json +4136 -0
  158. package/skills/codeply-taste/library/sites.json +7274 -0
  159. package/skills/codeply-taste/library/type.json +2024 -0
  160. package/skills/codeply-taste/references/colour.md +141 -0
  161. package/skills/codeply-taste/references/copy.md +177 -0
  162. package/skills/codeply-taste/references/critique.md +171 -0
  163. package/skills/codeply-taste/references/imagery.md +377 -0
  164. package/skills/codeply-taste/references/layout.md +178 -0
  165. package/skills/codeply-taste/references/motion.md +292 -0
  166. package/skills/codeply-taste/references/research.md +306 -0
  167. package/skills/codeply-taste/references/tells.md +115 -0
  168. package/skills/codeply-taste/references/type.md +170 -0
  169. package/skills/codeply-taste/scripts/assets.mjs +865 -0
  170. package/skills/codeply-taste/scripts/draw.mjs +525 -0
  171. package/skills/codeply-taste/scripts/inspect.mjs +205 -0
  172. package/skills/codeply-taste/scripts/lib/cdp.mjs +205 -0
  173. package/skills/codeply-taste/scripts/lib/probe.mjs +569 -0
  174. package/skills/codeply-taste/scripts/lib/rules.mjs +137 -0
  175. package/skills/codeply-taste/scripts/lint.mjs +108 -0
  176. package/skills/coding-standards/SKILL.md +551 -551
  177. package/skills/competitive-platform-analysis/SKILL.md +214 -214
  178. package/skills/competitive-report-structure/SKILL.md +162 -162
  179. package/skills/compose-multiplatform-patterns/SKILL.md +300 -300
  180. package/skills/config-gc/SKILL.md +120 -120
  181. package/skills/configure-ecc/SKILL.md +385 -385
  182. package/skills/connections-optimizer/SKILL.md +190 -190
  183. package/skills/content-engine/SKILL.md +132 -132
  184. package/skills/content-hash-cache-pattern/SKILL.md +162 -162
  185. package/skills/context-budget/SKILL.md +136 -136
  186. package/skills/continuous-agent-loop/SKILL.md +46 -46
  187. package/skills/continuous-learning/SKILL.md +132 -132
  188. package/skills/continuous-learning-v2/SKILL.md +361 -361
  189. package/skills/contract-first/SKILL.md +287 -287
  190. package/skills/cost-aware-llm-pipeline/SKILL.md +184 -184
  191. package/skills/cost-tracking/SKILL.md +97 -97
  192. package/skills/council/SKILL.md +204 -204
  193. package/skills/cpp-coding-standards/SKILL.md +724 -724
  194. package/skills/cpp-testing/SKILL.md +325 -325
  195. package/skills/crosspost/SKILL.md +112 -112
  196. package/skills/csharp-testing/SKILL.md +322 -322
  197. package/skills/customer-billing-ops/SKILL.md +141 -141
  198. package/skills/customs-trade-compliance/SKILL.md +263 -263
  199. package/skills/dart-flutter-patterns/SKILL.md +564 -564
  200. package/skills/dashboard-builder/SKILL.md +109 -109
  201. package/skills/data-scraper-agent/SKILL.md +765 -765
  202. package/skills/data-throughput-accelerator/SKILL.md +74 -74
  203. package/skills/database-migrations/SKILL.md +430 -430
  204. package/skills/deep-research/SKILL.md +160 -160
  205. package/skills/defi-amm-security/SKILL.md +167 -167
  206. package/skills/delivery-gate/SKILL.md +126 -126
  207. package/skills/deployment-patterns/SKILL.md +428 -428
  208. package/skills/design-system/SKILL.md +83 -83
  209. package/skills/django-celery/SKILL.md +458 -458
  210. package/skills/django-patterns/SKILL.md +735 -735
  211. package/skills/django-security/SKILL.md +644 -644
  212. package/skills/django-tdd/SKILL.md +730 -730
  213. package/skills/django-verification/SKILL.md +470 -470
  214. package/skills/dmux-workflows/SKILL.md +192 -192
  215. package/skills/docker-patterns/SKILL.md +365 -365
  216. package/skills/documentation-lookup/SKILL.md +91 -91
  217. package/skills/dotnet-patterns/SKILL.md +322 -322
  218. package/skills/dynamic-workflow-mode/SKILL.md +124 -124
  219. package/skills/e2e-testing/SKILL.md +327 -327
  220. package/skills/ecc-guide/SKILL.md +190 -190
  221. package/skills/ecc-recipes/SKILL.md +149 -149
  222. package/skills/ecc-tools-cost-audit/SKILL.md +161 -161
  223. package/skills/email-ops/SKILL.md +122 -122
  224. package/skills/energy-procurement/SKILL.md +228 -228
  225. package/skills/enterprise-agent-ops/SKILL.md +51 -51
  226. package/skills/error-handling/SKILL.md +377 -377
  227. package/skills/eval-harness/SKILL.md +271 -271
  228. package/skills/evm-token-decimals/SKILL.md +131 -131
  229. package/skills/exa-search/SKILL.md +108 -108
  230. package/skills/fal-ai-media/SKILL.md +289 -289
  231. package/skills/fastapi-patterns/SKILL.md +514 -514
  232. package/skills/finance-billing-ops/SKILL.md +128 -128
  233. package/skills/flox-environments/SKILL.md +497 -497
  234. package/skills/flutter-dart-code-review/SKILL.md +436 -436
  235. package/skills/foundation-models-on-device/SKILL.md +243 -243
  236. package/skills/frontend-a11y/SKILL.md +446 -446
  237. package/skills/frontend-design-direction/SKILL.md +93 -93
  238. package/skills/frontend-patterns/SKILL.md +657 -657
  239. package/skills/frontend-slides/SKILL.md +185 -185
  240. package/skills/fsharp-testing/SKILL.md +281 -281
  241. package/skills/gan-style-harness/SKILL.md +279 -279
  242. package/skills/gateguard/SKILL.md +133 -133
  243. package/skills/generating-python-installer/SKILL.md +820 -820
  244. package/skills/git-workflow/SKILL.md +716 -716
  245. package/skills/github-ops/SKILL.md +145 -145
  246. package/skills/golang-patterns/SKILL.md +676 -676
  247. package/skills/golang-testing/SKILL.md +721 -721
  248. package/skills/google-workspace-ops/SKILL.md +96 -96
  249. package/skills/growth-log/SKILL.md +128 -128
  250. package/skills/healthcare-cdss-patterns/SKILL.md +246 -246
  251. package/skills/healthcare-emr-patterns/SKILL.md +160 -160
  252. package/skills/healthcare-eval-harness/SKILL.md +208 -208
  253. package/skills/healthcare-phi-compliance/SKILL.md +146 -146
  254. package/skills/hermes-imports/SKILL.md +89 -89
  255. package/skills/hexagonal-architecture/SKILL.md +277 -277
  256. package/skills/hipaa-compliance/SKILL.md +79 -79
  257. package/skills/homelab-network-readiness/SKILL.md +170 -170
  258. package/skills/homelab-network-setup/SKILL.md +130 -130
  259. package/skills/homelab-pihole-dns/SKILL.md +275 -275
  260. package/skills/homelab-vlan-segmentation/SKILL.md +312 -312
  261. package/skills/homelab-wireguard-vpn/SKILL.md +306 -306
  262. package/skills/hookify-rules/SKILL.md +128 -128
  263. package/skills/inherit-legacy-style/SKILL.md +157 -157
  264. package/skills/intent-driven-development/SKILL.md +360 -360
  265. package/skills/inventory-demand-planning/SKILL.md +247 -247
  266. package/skills/investor-materials/SKILL.md +97 -97
  267. package/skills/investor-outreach/SKILL.md +92 -92
  268. package/skills/ios-icon-gen/SKILL.md +158 -158
  269. package/skills/iterative-retrieval/SKILL.md +212 -212
  270. package/skills/ito-basket-compare/SKILL.md +64 -64
  271. package/skills/ito-compute/SKILL.md +129 -129
  272. package/skills/ito-data-atlas-agent/SKILL.md +64 -64
  273. package/skills/ito-market-intelligence/SKILL.md +61 -61
  274. package/skills/ito-trade-planner/SKILL.md +68 -68
  275. package/skills/java-coding-standards/SKILL.md +384 -384
  276. package/skills/jira-integration/SKILL.md +303 -303
  277. package/skills/jpa-patterns/SKILL.md +152 -152
  278. package/skills/knowledge-ops/SKILL.md +155 -155
  279. package/skills/kotlin-coroutines-flows/SKILL.md +285 -285
  280. package/skills/kotlin-exposed-patterns/SKILL.md +720 -720
  281. package/skills/kotlin-ktor-patterns/SKILL.md +690 -690
  282. package/skills/kotlin-patterns/SKILL.md +712 -712
  283. package/skills/kotlin-testing/SKILL.md +825 -825
  284. package/skills/kubernetes-patterns/SKILL.md +756 -756
  285. package/skills/laravel-patterns/SKILL.md +416 -416
  286. package/skills/laravel-plugin-discovery/SKILL.md +230 -230
  287. package/skills/laravel-security/SKILL.md +948 -948
  288. package/skills/laravel-tdd/SKILL.md +675 -675
  289. package/skills/laravel-verification/SKILL.md +180 -180
  290. package/skills/latency-critical-systems/SKILL.md +75 -75
  291. package/skills/lead-intelligence/SKILL.md +322 -322
  292. package/skills/liquid-glass-design/SKILL.md +279 -279
  293. package/skills/llm-trading-agent-security/SKILL.md +147 -147
  294. package/skills/logistics-exception-management/SKILL.md +222 -222
  295. package/skills/loop-design-check/SKILL.md +143 -143
  296. package/skills/mailtrap-email-integration/SKILL.md +77 -77
  297. package/skills/make-interfaces-feel-better/SKILL.md +152 -152
  298. package/skills/manim-video/SKILL.md +90 -90
  299. package/skills/market-research/SKILL.md +76 -76
  300. package/skills/marketing-campaign/SKILL.md +114 -114
  301. package/skills/mcp-server-patterns/SKILL.md +70 -70
  302. package/skills/messages-ops/SKILL.md +105 -105
  303. package/skills/ml-adoption-playbook/SKILL.md +57 -57
  304. package/skills/mle-workflow/SKILL.md +348 -348
  305. package/skills/motion-advanced/SKILL.md +596 -596
  306. package/skills/motion-foundations/SKILL.md +299 -299
  307. package/skills/motion-patterns/SKILL.md +434 -434
  308. package/skills/motion-ui/SKILL.md +576 -576
  309. package/skills/mysql-patterns/SKILL.md +413 -413
  310. package/skills/nanoclaw-repl/SKILL.md +34 -34
  311. package/skills/nestjs-patterns/SKILL.md +231 -231
  312. package/skills/netmiko-ssh-automation/SKILL.md +174 -174
  313. package/skills/network-bgp-diagnostics/SKILL.md +168 -168
  314. package/skills/network-config-validation/SKILL.md +211 -211
  315. package/skills/network-interface-health/SKILL.md +153 -153
  316. package/skills/nextjs-turbopack/SKILL.md +58 -58
  317. package/skills/nodejs-keccak256/SKILL.md +103 -103
  318. package/skills/nutrient-document-processing/SKILL.md +168 -168
  319. package/skills/nuxt4-patterns/SKILL.md +101 -101
  320. package/skills/openclaw-persona-forge/SKILL.md +289 -289
  321. package/skills/openclaw-persona-forge/references/avatar-style.md +124 -124
  322. package/skills/openclaw-persona-forge/references/boundary-rules.md +53 -53
  323. package/skills/openclaw-persona-forge/references/error-handling.md +53 -53
  324. package/skills/openclaw-persona-forge/references/identity-tension.md +48 -48
  325. package/skills/openclaw-persona-forge/references/naming-system.md +39 -39
  326. package/skills/openclaw-persona-forge/references/output-template.md +166 -166
  327. package/skills/opensource-pipeline/SKILL.md +256 -256
  328. package/skills/orch-add-feature/SKILL.md +45 -45
  329. package/skills/orch-build-mvp/SKILL.md +49 -49
  330. package/skills/orch-change-feature/SKILL.md +43 -43
  331. package/skills/orch-fix-defect/SKILL.md +43 -43
  332. package/skills/orch-pipeline/SKILL.md +121 -121
  333. package/skills/orch-refine-code/SKILL.md +44 -44
  334. package/skills/parallel-execution-optimizer/SKILL.md +74 -74
  335. package/skills/perl-patterns/SKILL.md +505 -505
  336. package/skills/perl-security/SKILL.md +504 -504
  337. package/skills/perl-testing/SKILL.md +476 -476
  338. package/skills/plan-canvas/SKILL.md +153 -153
  339. package/skills/plan-orchestrate/SKILL.md +263 -263
  340. package/skills/plankton-code-quality/SKILL.md +237 -237
  341. package/skills/postgres-patterns/SKILL.md +148 -148
  342. package/skills/prediction-market-oracle-research/SKILL.md +64 -64
  343. package/skills/prediction-market-risk-review/SKILL.md +61 -61
  344. package/skills/premium-web-design/SKILL.md +154 -154
  345. package/skills/prisma-patterns/SKILL.md +401 -401
  346. package/skills/product-capability/SKILL.md +142 -142
  347. package/skills/product-lens/SKILL.md +93 -93
  348. package/skills/production-audit/SKILL.md +207 -207
  349. package/skills/production-scheduling/SKILL.md +238 -238
  350. package/skills/project-flow-ops/SKILL.md +112 -112
  351. package/skills/prompt-optimizer/SKILL.md +398 -398
  352. package/skills/python-patterns/SKILL.md +751 -751
  353. package/skills/python-testing/SKILL.md +817 -817
  354. package/skills/pytorch-patterns/SKILL.md +397 -397
  355. package/skills/quality-nonconformance/SKILL.md +260 -260
  356. package/skills/quarkus-patterns/SKILL.md +723 -723
  357. package/skills/quarkus-security/SKILL.md +468 -468
  358. package/skills/quarkus-tdd/SKILL.md +812 -812
  359. package/skills/quarkus-verification/SKILL.md +480 -480
  360. package/skills/ralphinho-rfc-pipeline/SKILL.md +68 -68
  361. package/skills/react-native-patterns/SKILL.md +326 -326
  362. package/skills/react-patterns/SKILL.md +342 -342
  363. package/skills/react-performance/SKILL.md +575 -575
  364. package/skills/react-testing/SKILL.md +424 -424
  365. package/skills/recsys-pipeline-architect/SKILL.md +115 -115
  366. package/skills/recursive-decision-ledger/SKILL.md +81 -81
  367. package/skills/redis-patterns/SKILL.md +404 -404
  368. package/skills/regex-vs-llm-structured-text/SKILL.md +221 -221
  369. package/skills/remotion-video-creation/SKILL.md +43 -43
  370. package/skills/repo-scan/SKILL.md +79 -79
  371. package/skills/research-ops/SKILL.md +113 -113
  372. package/skills/returns-reverse-logistics/SKILL.md +240 -240
  373. package/skills/rules-distill/SKILL.md +265 -265
  374. package/skills/rust-patterns/SKILL.md +500 -500
  375. package/skills/rust-testing/SKILL.md +501 -501
  376. package/skills/safety-guard/SKILL.md +76 -76
  377. package/skills/santa-method/SKILL.md +307 -307
  378. package/skills/scientific-db-pubmed-database/SKILL.md +176 -176
  379. package/skills/scientific-db-uspto-database/SKILL.md +178 -178
  380. package/skills/scientific-pkg-gget/SKILL.md +167 -167
  381. package/skills/scientific-thinking-literature-review/SKILL.md +193 -193
  382. package/skills/scientific-thinking-scholar-evaluation/SKILL.md +161 -161
  383. package/skills/search-first/SKILL.md +183 -183
  384. package/skills/security-bounty-hunter/SKILL.md +100 -100
  385. package/skills/security-review/SKILL.md +504 -504
  386. package/skills/security-scan/SKILL.md +166 -166
  387. package/skills/seo/SKILL.md +155 -155
  388. package/skills/skill-comply/SKILL.md +59 -59
  389. package/skills/skill-scout/SKILL.md +141 -141
  390. package/skills/skill-stocktake/SKILL.md +195 -195
  391. package/skills/social-graph-ranker/SKILL.md +155 -155
  392. package/skills/social-publisher/SKILL.md +130 -130
  393. package/skills/springboot-patterns/SKILL.md +315 -315
  394. package/skills/springboot-security/SKILL.md +273 -273
  395. package/skills/springboot-tdd/SKILL.md +159 -159
  396. package/skills/springboot-verification/SKILL.md +232 -232
  397. package/skills/strategic-compact/SKILL.md +142 -142
  398. package/skills/swift-actor-persistence/SKILL.md +144 -144
  399. package/skills/swift-concurrency-6-2/SKILL.md +216 -216
  400. package/skills/swift-protocol-di-testing/SKILL.md +191 -191
  401. package/skills/swiftui-patterns/SKILL.md +259 -259
  402. package/skills/taste/SKILL.md +264 -264
  403. package/skills/taste/references/genre-taxonomy.md +87 -87
  404. package/skills/tdd-workflow/SKILL.md +583 -583
  405. package/skills/team-agent-orchestration/SKILL.md +111 -111
  406. package/skills/team-builder/SKILL.md +169 -169
  407. package/skills/terminal-ops/SKILL.md +110 -110
  408. package/skills/tinystruct-patterns/SKILL.md +279 -279
  409. package/skills/tinystruct-patterns/references/architecture.md +90 -90
  410. package/skills/tinystruct-patterns/references/data-handling.md +60 -60
  411. package/skills/tinystruct-patterns/references/database.md +99 -99
  412. package/skills/tinystruct-patterns/references/routing.md +64 -64
  413. package/skills/tinystruct-patterns/references/system-usage.md +97 -97
  414. package/skills/tinystruct-patterns/references/testing.md +72 -72
  415. package/skills/token-budget-advisor/SKILL.md +134 -134
  416. package/skills/ui-demo/SKILL.md +466 -466
  417. package/skills/ui-to-vue/SKILL.md +135 -135
  418. package/skills/uncloud/SKILL.md +344 -344
  419. package/skills/unified-memory/SKILL.md +170 -170
  420. package/skills/unified-notifications-ops/SKILL.md +188 -188
  421. package/skills/verification-loop/SKILL.md +129 -129
  422. package/skills/video-editing/SKILL.md +311 -311
  423. package/skills/videodb/SKILL.md +375 -375
  424. package/skills/visa-doc-translate/SKILL.md +117 -117
  425. package/skills/vite-patterns/SKILL.md +450 -450
  426. package/skills/vue-patterns/SKILL.md +471 -471
  427. package/skills/windows-desktop-e2e/SKILL.md +888 -888
  428. package/skills/workspace-surface-audit/SKILL.md +126 -126
  429. 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: 'INBOX', maxResults: 100, pageToken })}`);
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;