openmausbot 0.1.85 → 0.1.87

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 (262) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  3. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  4. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  5. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  6. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  11. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  12. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  17. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  18. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  19. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  20. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  21. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  22. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  23. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  27. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  28. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  29. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  30. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  31. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  32. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  33. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  34. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  35. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  36. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  44. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  45. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  46. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  47. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  48. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  49. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  50. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  51. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  52. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  53. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  54. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  55. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  56. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  57. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  58. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  59. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  60. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  61. package/dist/assets/abnfDiagram-O67JEVCF-BTiuL79F.js +1 -0
  62. package/dist/assets/arc-C88Blc2v.js +1 -0
  63. package/dist/assets/architectureDiagram-NJMV4G6O-BZsJIZrb.js +36 -0
  64. package/dist/assets/blockDiagram-BEXU5L5S-CVeMe9Ah.js +140 -0
  65. package/dist/assets/c4Diagram-YGBWAQC7-Cedk3Bhx.js +38 -0
  66. package/dist/assets/channel-taCNRlUU.js +1 -0
  67. package/dist/assets/chunk-2Q5K7J3B-D-VRqzTB.js +1 -0
  68. package/dist/assets/chunk-3FUC2YCW-BChov01Y.js +2 -0
  69. package/dist/assets/chunk-5VM5RSS4-C-TfwvHy.js +15 -0
  70. package/dist/assets/chunk-GTNCS2PH-Db7CGj5Y.js +88 -0
  71. package/dist/assets/chunk-GWA4HPMP-CNvIfqKT.js +1 -0
  72. package/dist/assets/chunk-JWPE2WC7-C_bEp5tq.js +1 -0
  73. package/dist/assets/chunk-NETBCI7D-Coi4BOls.js +1 -0
  74. package/dist/assets/chunk-WEXAMYUT-CzylEuL3.js +1 -0
  75. package/dist/assets/chunk-XXDRQBXY-m7p7ztaD.js +1 -0
  76. package/dist/assets/classDiagram-v2-NBCMYWYE-BMy6o9x2.js +217 -0
  77. package/dist/assets/cose-bilkent-JH36ORCC-BDQ__0_J.js +1 -0
  78. package/dist/assets/cynefin-OW5HDTMX-Djv4ghUb.js +166 -0
  79. package/dist/assets/cynefinDiagram-VND7K2PF-BULrlGYH.js +62 -0
  80. package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
  81. package/dist/assets/dagre-6A5THRUB-D-jrT_hU.js +4 -0
  82. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  83. package/dist/assets/diagram-22UHCM2B-p2UhQmfL.js +200 -0
  84. package/dist/assets/diagram-3UASUU5V-DHvGWZ7q.js +24 -0
  85. package/dist/assets/diagram-ATOU4E4O-ChhKDot-.js +3 -0
  86. package/dist/assets/diagram-CDSNMT55-B-T7A43R.js +30 -0
  87. package/dist/assets/diagram-MLGK6HIB-CJm8ENNY.js +24 -0
  88. package/dist/assets/diagram-MPIPVDR6-510tlza8.js +41 -0
  89. package/dist/assets/ebnfDiagram-ZINNZB2B-PkvRPHel.js +1 -0
  90. package/dist/assets/elk-276RUBZZ-BVBVPoX3.js +27 -0
  91. package/dist/assets/erDiagram-OPXOYQCR-BgYMVm7L.js +99 -0
  92. package/dist/assets/flowDiagram-KWPJA3E3-BGqsnc_3.js +213 -0
  93. package/dist/assets/ganttDiagram-FUAMR5RP-D_-UeReH.js +292 -0
  94. package/dist/assets/gitGraphDiagram-X574FWY7-D0uooONx.js +106 -0
  95. package/dist/assets/graph-D8UmR1vK.js +1 -0
  96. package/dist/assets/index-BwdMAQ6p.css +1 -0
  97. package/dist/assets/index-ByJennr7.js +581 -0
  98. package/dist/assets/{index-CTNNoqSJ.js → index-zRsJERGJ.js} +1 -1
  99. package/dist/assets/infoDiagram-VRGFBTTK-B9e1z77f.js +2 -0
  100. package/dist/assets/init-Gi6I4Gst.js +1 -0
  101. package/dist/assets/ishikawaDiagram-OU5B5YK6-BB4vNwvD.js +70 -0
  102. package/dist/assets/journeyDiagram-ZHPQQLJL-DpkosOTB.js +139 -0
  103. package/dist/assets/kanban-definition-PNTS6WVX-DbDP4kUu.js +89 -0
  104. package/dist/assets/linear-DBJGPmjT.js +1 -0
  105. package/dist/assets/mermaid.core-DWPRi-ue.js +311 -0
  106. package/dist/assets/mindmap-definition-NLK3R4M7-nszBbJmx.js +96 -0
  107. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  108. package/dist/assets/pegDiagram-GJSIUBJH-CWykJWu_.js +1 -0
  109. package/dist/assets/pieDiagram-5QR66LMP-g3Da-Vnv.js +39 -0
  110. package/dist/assets/quadrantDiagram-O4NWA36T-Wzrlpzgh.js +7 -0
  111. package/dist/assets/railroadDiagram-XR7U4H2S-moHTFMC0.js +1 -0
  112. package/dist/assets/requirementDiagram-PLB6GJNP-CwfXdFH8.js +84 -0
  113. package/dist/assets/sankeyDiagram-IPEJSGJF-3ZROYvnq.js +40 -0
  114. package/dist/assets/sequenceDiagram-PO4LG4MO-E1TBebP8.js +169 -0
  115. package/dist/assets/sizeCapture-INFHLROL-DHKEU5pu.js +1 -0
  116. package/dist/assets/stateDiagram-v2-GCMORJYK-BguEj09D.js +291 -0
  117. package/dist/assets/swimlanes-2SLR337P-C7IDYnhd.js +1 -0
  118. package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
  119. package/dist/assets/timeline-definition-EJHVYXUP-DCW-F5PW.js +120 -0
  120. package/dist/assets/usecaseDiagram-POWQR4AR-xBMFtWYV.js +370 -0
  121. package/dist/assets/vennDiagram-UO4OBE2U-B5nIkuH8.js +34 -0
  122. package/dist/assets/wardleyDiagram-VNRHLVJA-DYFAhdbr.js +78 -0
  123. package/dist/assets/xychartDiagram-PMCCYNJV-DeXwX6Cz.js +7 -0
  124. package/dist/index.html +2 -2
  125. package/dist-server/browser-proxy.js +2 -1
  126. package/dist-server/container-mcp.js +1047 -173
  127. package/dist-server/drivers/agents-proxy.js +16121 -1504
  128. package/dist-server/drivers/phone-proxy.js +33 -2
  129. package/dist-server/enterprise/LICENSE +32 -0
  130. package/dist-server/enterprise/server/index.js +5 -5
  131. package/dist-server/index.js +16134 -7446
  132. package/dist-server/local-computer-proxy.js +3 -3
  133. package/dist-server/local-computer.js +53 -17
  134. package/dist-server/mcp-server.js +3 -1
  135. package/dist-server/openmausbot.js +9353 -6124
  136. package/dist-server/pair-cli.js +9353 -6124
  137. package/dist-server/permission-proxy.js +29 -9
  138. package/dist-server/server/admin-activity.js +410 -0
  139. package/dist-server/server/attachments.js +55 -0
  140. package/dist-server/server/auto-approve.js +4 -3
  141. package/dist-server/server/auto-vm-claims.js +1 -0
  142. package/dist-server/server/bot-package.js +18 -292
  143. package/dist-server/server/bot-profile.js +5 -17
  144. package/dist-server/server/bot-visibility.js +462 -0
  145. package/dist-server/server/box-create-idempotency.js +3 -3
  146. package/dist-server/server/box.js +59 -56
  147. package/dist-server/server/browser-proxy.js +3 -2
  148. package/dist-server/server/browser-runtime.js +38 -2
  149. package/dist-server/server/channel-queue.js +2 -0
  150. package/dist-server/server/cli-prompts.js +24 -2
  151. package/dist-server/server/cli.js +81 -12
  152. package/dist-server/server/command-allowlist.js +154 -0
  153. package/dist-server/server/composio.js +49 -11
  154. package/dist-server/server/computer-wait.js +22 -9
  155. package/dist-server/server/config.js +133 -17
  156. package/dist-server/server/connector-verdict.js +141 -0
  157. package/dist-server/server/container-computer.js +1 -1
  158. package/dist-server/server/decision-log.js +209 -48
  159. package/dist-server/server/default-model-selection.js +29 -3
  160. package/dist-server/server/delegations.js +97 -44
  161. package/dist-server/server/drivers/acp/core.js +163 -33
  162. package/dist-server/server/drivers/acp/grok.js +4 -3
  163. package/dist-server/server/drivers/agents-call.js +1162 -0
  164. package/dist-server/server/drivers/agents-catalog.js +689 -0
  165. package/dist-server/server/drivers/agents-client.js +28 -0
  166. package/dist-server/server/drivers/agents-proxy.js +18 -1722
  167. package/dist-server/server/drivers/boxagent.js +174 -36
  168. package/dist-server/server/drivers/builtIn.js +2 -0
  169. package/dist-server/server/drivers/chat-box-tools.js +90 -0
  170. package/dist-server/server/drivers/chat-images.js +54 -0
  171. package/dist-server/server/drivers/chat-mcp-tools.js +55 -10
  172. package/dist-server/server/drivers/chat-tool-approval.js +70 -18
  173. package/dist-server/server/drivers/claude.js +28 -8
  174. package/dist-server/server/drivers/codex-instructions.js +48 -6
  175. package/dist-server/server/drivers/codex-mcp-names.js +44 -0
  176. package/dist-server/server/drivers/codex-tool-surface.js +20 -0
  177. package/dist-server/server/drivers/codex.js +121 -34
  178. package/dist-server/server/drivers/grok.js +2 -1
  179. package/dist-server/server/drivers/mistral.js +115 -0
  180. package/dist-server/server/drivers/openai-chat.js +155 -32
  181. package/dist-server/server/drivers/openai-compat.js +16 -8
  182. package/dist-server/server/drivers/permission-command.js +35 -0
  183. package/dist-server/server/drivers/phone-proxy.js +47 -2
  184. package/dist-server/server/drivers/pi.js +75 -4
  185. package/dist-server/server/drivers/prompt-split.js +87 -0
  186. package/dist-server/server/enterprise.js +97 -16
  187. package/dist-server/server/external-runtime.js +83 -0
  188. package/dist-server/server/fleet.js +1 -1
  189. package/dist-server/server/group-thread-usage.js +103 -0
  190. package/dist-server/server/guarded-requests.js +60 -0
  191. package/dist-server/server/harness/http.js +61 -0
  192. package/dist-server/server/harness/registry.js +1 -0
  193. package/dist-server/server/hosted-models.js +151 -0
  194. package/dist-server/server/hosted-slack.js +11 -0
  195. package/dist-server/server/index.js +3091 -658
  196. package/dist-server/server/local-computer.js +72 -23
  197. package/dist-server/server/managed-desktop.js +79 -14
  198. package/dist-server/server/managed-policy.js +153 -0
  199. package/dist-server/server/mcp-bridge.js +6 -6
  200. package/dist-server/server/message-db.js +28 -0
  201. package/dist-server/server/model-context-window.js +7 -0
  202. package/dist-server/server/model-prices.js +121 -0
  203. package/dist-server/server/new-bot-defaults.js +115 -0
  204. package/dist-server/server/notify.js +10 -1
  205. package/dist-server/server/org-library.js +1051 -0
  206. package/dist-server/server/package-export.js +627 -39
  207. package/dist-server/server/package-import.js +458 -0
  208. package/dist-server/server/package-parts.js +87 -0
  209. package/dist-server/server/peer-approval.js +16 -7
  210. package/dist-server/server/peer-roster.js +23 -2
  211. package/dist-server/server/presets.js +492 -0
  212. package/dist-server/server/provider-key-check.js +3 -2
  213. package/dist-server/server/published-teams.js +80 -0
  214. package/dist-server/server/recent-work.js +15 -0
  215. package/dist-server/server/request-auth.js +96 -0
  216. package/dist-server/server/room-handoffs.js +44 -14
  217. package/dist-server/server/routes/bot-presets.js +25 -0
  218. package/dist-server/server/routes/hosted-slack.js +20 -0
  219. package/dist-server/server/routes/table.js +14 -0
  220. package/dist-server/server/routine-requests.js +6 -3
  221. package/dist-server/server/routines.js +63 -1
  222. package/dist-server/server/section-context.js +10 -0
  223. package/dist-server/server/skills.js +34 -61
  224. package/dist-server/server/spend.js +111 -16
  225. package/dist-server/server/steer-queue.js +9 -3
  226. package/dist-server/server/store.js +550 -35
  227. package/dist-server/server/surface.js +10 -4
  228. package/dist-server/server/system-prompt.js +12 -4
  229. package/dist-server/server/team-backup.js +16 -3
  230. package/dist-server/server/team-computers.js +10 -0
  231. package/dist-server/server/team-library.js +6 -2
  232. package/dist-server/server/team-manifest.js +4 -3
  233. package/dist-server/server/team-setup-requests.js +16 -5
  234. package/dist-server/server/thread-events.js +17 -1
  235. package/dist-server/server/thread-starters.js +53 -0
  236. package/dist-server/server/turn-context.js +2 -0
  237. package/dist-server/server/turn-watchdog.js +12 -3
  238. package/dist-server/server/usage-ledger.js +43 -9
  239. package/dist-server/server/vps-computer.js +139 -51
  240. package/dist-server/server/vps-container-mcp.js +7 -0
  241. package/dist-server/server/vps-ssh.js +36 -7
  242. package/dist-server/server/workspace-backup-policy.js +21 -2
  243. package/dist-server/server/workspace-backup.js +7 -4
  244. package/dist-server/server/workspace.js +1 -1
  245. package/dist-server/shared/approval-mode.js +1 -1
  246. package/dist-server/shared/ask-question.js +188 -12
  247. package/dist-server/shared/bot-profile.js +15 -0
  248. package/dist-server/shared/command-allowlist.js +1 -0
  249. package/dist-server/shared/credential-request.js +1 -1
  250. package/dist-server/shared/mention-boundary.js +15 -0
  251. package/dist-server/shared/package-format.js +967 -0
  252. package/dist-server/shared/routines.js +7 -1
  253. package/dist-server/shared/skill-md.js +64 -0
  254. package/dist-server/shared/team-backup.js +9 -0
  255. package/dist-server/shared/wire.js +4 -0
  256. package/dist-server/src/lib/composer-attachments.js +16 -0
  257. package/dist-server/vps-container-mcp.js +1147 -179
  258. package/enterprise/README.md +33 -12
  259. package/enterprise/server/index.js +5 -5
  260. package/package.json +1 -1
  261. package/dist/assets/index-Ba9G44HI.js +0 -310
  262. package/dist/assets/index-CkRNp7wX.css +0 -1
@@ -0,0 +1,492 @@
1
+ // Preset bots: named starting points in the New bot dialog, carried by a
2
+ // shared package (a file someone sent, or the organization's shelf).
3
+ //
4
+ // A preset is an ALLOWLIST (critique #3): a name and a short description,
5
+ // the new bot's name, title, description, standing instructions and look,
6
+ // playbooks, skills (SKILL.md text only) and starter notes. It has no field
7
+ // for a model, a folder, a computer, an approval level, connected apps, MCP
8
+ // servers, a browser or anything else that grants reach, so nothing a
9
+ // preset says can widen what a bot may do. The person creating the bot
10
+ // keeps their own choices for all of that.
11
+ //
12
+ // Where presets live: `DATA_DIR/org-library/presets.json` (contract §3.4).
13
+ // One row per preset, stamped with where it came from ("file" or "org") and
14
+ // the install it belongs to, plus the skill and playbook definitions each
15
+ // install's presets use, so a bot can be created from a preset long after
16
+ // the file or the organization's release is gone. Every use re-reads a row
17
+ // through the same package parser every import uses, so a hand-edited file
18
+ // can still only say what the package format allows.
19
+ //
20
+ // Trust is where the preset came from, never what it says: skills from an
21
+ // organization preset are added switched on (the organization's Admin
22
+ // published them), skills from a file preset switched off. Starter notes are
23
+ // written once, through the normal memory writers (which scrub secrets).
24
+ //
25
+ // There is no confirm step anywhere: choosing a preset in New bot and
26
+ // pressing Create is the decision.
27
+ import { randomUUID } from "node:crypto";
28
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
29
+ import { dirname, join } from "node:path";
30
+ import { z } from "zod";
31
+ import { writeFileAtomic } from "./atomic.js";
32
+ import { DATA_DIR } from "./config.js";
33
+ import { AGENT_MAX_SKILLS, canonicalJson, PACKAGE_FORMAT, PACKAGE_VERSION, parsePackageDocument, SEED_FILE_MAX_BYTES, SEED_MAX_BYTES, SEED_MAX_FILES, utf8Bytes, } from "../shared/package-format.js";
34
+ import { parseSkillMd } from "../shared/skill-md.js";
35
+ export const PRESETS_FILE = join(DATA_DIR, "org-library", "presets.json");
36
+ /** The organization library's state (W2-1, contract §3.4); read only here. */
37
+ export const ORG_LIBRARY_STATE_FILE = join(DATA_DIR, "org-library", "state.json");
38
+ /** What this installation last shared as a preset file (skills + presets, no team). */
39
+ export const PUBLISHED_LIBRARY_FILE = join(DATA_DIR, "published-library.json");
40
+ /** Enough for every preset of ten full packages; a runaway import cannot grow the file without bound. */
41
+ export const MAX_STORED_PRESETS = 200;
42
+ /** The key "Include my New bot defaults as a preset" is shared under. Fixed,
43
+ * so renaming the preset keeps its identity across releases. */
44
+ export const DEFAULTS_PRESET_KEY = "new-bot-defaults";
45
+ export const PRESET_UNAVAILABLE_MESSAGE = "That preset is no longer available. Choose another starting role.";
46
+ export const ORG_PRESET_REMOVE_MESSAGE = "Presets from your organization are managed in Admin.";
47
+ // ── the stored file ─────────────────────────────────────────────────────────
48
+ const text = (max) => z.string().max(max);
49
+ const storedPresetSchema = z.object({
50
+ id: z.string().regex(/^[\w-]{1,64}$/),
51
+ source: z.enum(["file", "org"]),
52
+ installId: text(100).min(1),
53
+ packageId: text(80).min(1),
54
+ packageName: text(100).min(1),
55
+ release: text(30).min(1),
56
+ publisherName: text(100).optional(),
57
+ publisher: z.object({ organizationId: text(200), slug: text(40), name: text(100) }).optional(),
58
+ ref: text(120).optional(),
59
+ sha256: text(64).optional(),
60
+ key: text(64).min(1),
61
+ name: text(100).min(1),
62
+ description: text(300).optional(),
63
+ // Checked field by field through the package parser on every use.
64
+ bot: z.record(z.string(), z.unknown()),
65
+ playbooks: z.array(z.string()).optional(),
66
+ skills: z.array(z.string()).optional(),
67
+ seed: z.object({ memory: z.record(z.string(), z.string()) }).optional(),
68
+ addedAt: z.number(),
69
+ });
70
+ const fileSchema = z.object({
71
+ version: z.literal(1),
72
+ presets: z.array(z.unknown()),
73
+ content: z.record(z.string(), z.object({ skills: z.array(z.unknown()), playbooks: z.array(z.unknown()) })).optional(),
74
+ });
75
+ /** A preset exactly as the package carried it: what `preset:<key>` hashes
76
+ * over (contract §1.6), with the row's bookkeeping left out. */
77
+ export function storedPresetObject(row) {
78
+ return {
79
+ key: row.key,
80
+ name: row.name,
81
+ ...(row.description !== undefined ? { description: row.description } : {}),
82
+ bot: row.bot,
83
+ ...(row.playbooks ? { playbooks: row.playbooks } : {}),
84
+ ...(row.skills ? { skills: row.skills } : {}),
85
+ ...(row.seed ? { seed: row.seed } : {}),
86
+ };
87
+ }
88
+ function resolveRow(row, content) {
89
+ const wantedSkills = new Set(row.skills ?? []);
90
+ const wantedPlaybooks = new Set(row.playbooks ?? []);
91
+ const skills = (content?.skills ?? []).filter((skill) => wantedSkills.has(skill.name));
92
+ const playbooks = (content?.playbooks ?? []).filter((playbook) => wantedPlaybooks.has(playbook.key));
93
+ let document;
94
+ try {
95
+ document = parsePackageDocument({
96
+ format: PACKAGE_FORMAT,
97
+ version: PACKAGE_VERSION,
98
+ package: {
99
+ id: row.packageId,
100
+ release: row.release,
101
+ name: row.packageName,
102
+ tagline: row.packageName,
103
+ summary: row.packageName,
104
+ category: "Presets",
105
+ author: { name: row.publisherName ?? row.packageName },
106
+ license: "Unspecified",
107
+ outcomes: [row.name],
108
+ setupMinutes: 1,
109
+ requirements: { apps: [], capabilities: [] },
110
+ presets: [storedPresetObject(row)],
111
+ ...(skills.length ? { skills: { version: 1, entries: skills } } : {}),
112
+ ...(playbooks.length ? { playbooks } : {}),
113
+ },
114
+ }, { trust: "file" });
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ const pkg = document.package;
120
+ return { row, preset: pkg.presets[0], skills: pkg.skills?.entries ?? [], playbooks: pkg.playbooks ?? [] };
121
+ }
122
+ export function createPresetStore(file = PRESETS_FILE) {
123
+ const load = () => {
124
+ const empty = { version: 1, presets: [], content: Object.create(null) };
125
+ if (!existsSync(file))
126
+ return empty;
127
+ try {
128
+ const parsed = fileSchema.parse(JSON.parse(readFileSync(file, "utf8")));
129
+ const presets = parsed.presets.flatMap((value) => {
130
+ const row = storedPresetSchema.safeParse(value);
131
+ return row.success ? [row.data] : [];
132
+ });
133
+ const content = Object.create(null);
134
+ // Own entries only: an install id is data from a file.
135
+ for (const [installId, value] of Object.entries(parsed.content ?? {})) {
136
+ content[installId] = { skills: value.skills, playbooks: value.playbooks };
137
+ }
138
+ return { version: 1, presets, content };
139
+ }
140
+ catch {
141
+ // An unreadable file offers no presets. It is left alone (never
142
+ // overwritten by a read), so nothing is lost to a transient error.
143
+ return empty;
144
+ }
145
+ };
146
+ const save = (state) => {
147
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
148
+ // Content no preset uses any more is dropped with its last preset.
149
+ const used = new Set(state.presets.map((row) => row.installId));
150
+ const content = Object.create(null);
151
+ for (const [installId, value] of Object.entries(state.content))
152
+ if (used.has(installId))
153
+ content[installId] = value;
154
+ writeFileAtomic(file, JSON.stringify({ version: 1, presets: state.presets, content }, null, 2), { mode: 0o600 });
155
+ };
156
+ const sameFilePreset = (row, candidate, state, content) => row.source === "file" && candidate.source === "file" && row.packageId === candidate.packageId && row.release === candidate.release && row.key === candidate.key &&
157
+ canonicalJson(storedPresetObject(row)) === canonicalJson(storedPresetObject(candidate)) &&
158
+ canonicalJson(usedContent(row, state.content[row.installId])) === canonicalJson(usedContent(candidate, content));
159
+ return {
160
+ list: () => load().presets,
161
+ resolve(id) {
162
+ const state = load();
163
+ const row = state.presets.find((candidate) => candidate.id === id);
164
+ return row ? resolveRow(row, state.content[row.installId]) : null;
165
+ },
166
+ register(document, context) {
167
+ const pkg = document.package;
168
+ const presets = pkg.presets ?? [];
169
+ const added = [];
170
+ const existing = [];
171
+ const skipped = [];
172
+ const org = context.source === "org" ? context.org : undefined;
173
+ if (!presets.length && !org)
174
+ return { added, existing, skipped };
175
+ const state = load();
176
+ const used = new Set(presets.flatMap((preset) => preset.skills ?? []));
177
+ const usedPlaybooks = new Set(presets.flatMap((preset) => preset.playbooks ?? []));
178
+ const content = {
179
+ skills: (pkg.skills?.entries ?? []).filter((skill) => used.has(skill.name)),
180
+ playbooks: (pkg.playbooks ?? []).filter((playbook) => usedPlaybooks.has(playbook.key)),
181
+ };
182
+ // An organization install owns one set of rows. Adding it again (a
183
+ // removed team re-added, or an Add retried after the app stopped
184
+ // before state.json was written) refreshes them in place, keeping
185
+ // their ids, and drops keys the release no longer has. Nothing is
186
+ // duplicated, and nothing here decides whether it was added before:
187
+ // that is org-library.ts's index, rebuilt from the records.
188
+ const ownRow = (row) => row.source === "org" && row.installId === context.installId;
189
+ let refreshed = false;
190
+ if (org) {
191
+ const keys = new Set(presets.map((preset) => preset.key));
192
+ const kept = state.presets.filter((row) => !ownRow(row) || keys.has(row.key));
193
+ refreshed = kept.length !== state.presets.length;
194
+ state.presets = kept;
195
+ }
196
+ for (const preset of presets) {
197
+ const row = {
198
+ id: randomUUID(),
199
+ source: context.source,
200
+ installId: context.installId,
201
+ packageId: pkg.id,
202
+ packageName: pkg.name,
203
+ release: pkg.release,
204
+ ...(org ? { publisherName: org.publisher.name, publisher: { ...org.publisher }, ref: org.ref, sha256: org.sha256 } : {}),
205
+ key: preset.key,
206
+ name: preset.name,
207
+ ...(preset.description !== undefined ? { description: preset.description } : {}),
208
+ bot: structuredClone(preset.bot),
209
+ ...(preset.playbooks ? { playbooks: [...preset.playbooks] } : {}),
210
+ ...(preset.skills ? { skills: [...preset.skills] } : {}),
211
+ ...(preset.seed ? { seed: structuredClone(preset.seed) } : {}),
212
+ addedAt: Date.now(),
213
+ };
214
+ const own = org ? state.presets.findIndex((candidate) => ownRow(candidate) && candidate.key === preset.key) : -1;
215
+ if (own >= 0) {
216
+ const previous = state.presets[own];
217
+ state.presets[own] = { ...row, id: previous.id, addedAt: previous.addedAt };
218
+ existing.push({ id: previous.id, key: row.key, name: row.name });
219
+ refreshed = true;
220
+ continue;
221
+ }
222
+ // The same file added twice offers its presets once.
223
+ const same = state.presets.find((candidate) => sameFilePreset(candidate, row, state, content));
224
+ if (same) {
225
+ existing.push({ id: same.id, key: same.key, name: same.name });
226
+ continue;
227
+ }
228
+ if (state.presets.length >= MAX_STORED_PRESETS) {
229
+ skipped.push({ part: `presets[${preset.key}]`, reason: "too_many_presets" });
230
+ continue;
231
+ }
232
+ state.presets.push(row);
233
+ added.push({ id: row.id, key: row.key, name: row.name });
234
+ }
235
+ if (added.length || refreshed) {
236
+ state.content[context.installId] = content;
237
+ save(state);
238
+ }
239
+ return { added, existing, skipped };
240
+ },
241
+ remove(ids) {
242
+ if (!ids.length)
243
+ return;
244
+ const drop = new Set(ids);
245
+ const state = load();
246
+ const kept = state.presets.filter((row) => !drop.has(row.id));
247
+ if (kept.length === state.presets.length)
248
+ return;
249
+ save({ ...state, presets: kept });
250
+ },
251
+ removeFilePreset(id) {
252
+ const state = load();
253
+ const row = state.presets.find((candidate) => candidate.id === id);
254
+ if (!row)
255
+ return "not_found";
256
+ if (row.source !== "file")
257
+ return "organization";
258
+ save({ ...state, presets: state.presets.filter((candidate) => candidate.id !== id) });
259
+ return "removed";
260
+ },
261
+ };
262
+ }
263
+ function usedContent(row, content) {
264
+ const skills = new Set(row.skills ?? []);
265
+ const playbooks = new Set(row.playbooks ?? []);
266
+ return {
267
+ skills: (content?.skills ?? []).filter((skill) => skills.has(skill.name)),
268
+ playbooks: (content?.playbooks ?? []).filter((playbook) => playbooks.has(playbook.key)),
269
+ };
270
+ }
271
+ /** Each organization install's status from `org-library/state.json`
272
+ * (contract §3.4). No file, or an unreadable one, is an empty map: an
273
+ * organization preset is then shown like any copy that stays after sign-out. */
274
+ export function readOrgInstallStatuses(file = ORG_LIBRARY_STATE_FILE) {
275
+ const statuses = new Map();
276
+ try {
277
+ const state = JSON.parse(readFileSync(file, "utf8"));
278
+ if (!state.installs || typeof state.installs !== "object" || Array.isArray(state.installs))
279
+ return statuses;
280
+ for (const [installId, value] of Object.entries(state.installs)) {
281
+ const status = value && typeof value === "object" ? value.status : undefined;
282
+ if (status === "installed" || status === "withdrawn" || status === "removed")
283
+ statuses.set(installId, status);
284
+ }
285
+ }
286
+ catch {
287
+ /* no organization state yet */
288
+ }
289
+ return statuses;
290
+ }
291
+ /** Whether New bot may use this preset: not one from an organization
292
+ * release that was withdrawn or an install that was removed. */
293
+ export function presetOffered(row, statuses) {
294
+ if (row.source !== "org")
295
+ return true;
296
+ const status = statuses.get(row.installId);
297
+ return status !== "withdrawn" && status !== "removed";
298
+ }
299
+ /** Every preset the New bot dialog offers: the organization's first (by
300
+ * publisher, then name), then imported files (newest first). A preset from a
301
+ * release the organization withdrew, or an install removed, is not offered. */
302
+ export function listBotPresets(store, statuses) {
303
+ const wire = [];
304
+ for (const row of store.list()) {
305
+ if (!presetOffered(row, statuses))
306
+ continue;
307
+ const resolved = store.resolve(row.id);
308
+ if (!resolved)
309
+ continue;
310
+ const { preset } = resolved;
311
+ wire.push({
312
+ id: row.id,
313
+ source: row.source,
314
+ key: preset.key,
315
+ name: preset.name,
316
+ ...(preset.description ? { description: preset.description } : {}),
317
+ packageName: row.packageName,
318
+ release: row.release,
319
+ ...(row.source === "org" && row.publisherName ? { publisherName: row.publisherName } : {}),
320
+ bot: preset.bot,
321
+ skills: resolved.skills.map((skill) => ({ name: skill.name, description: skill.description })),
322
+ skillsEnabled: row.source === "org",
323
+ playbooks: resolved.playbooks.map((playbook) => playbook.name),
324
+ notes: Object.keys(preset.seed?.memory ?? {}),
325
+ addedAt: row.addedAt,
326
+ });
327
+ }
328
+ const org = wire.filter((preset) => preset.source === "org")
329
+ .sort((a, b) => (a.publisherName ?? "").localeCompare(b.publisherName ?? "") || a.name.localeCompare(b.name));
330
+ const files = wire.filter((preset) => preset.source === "file").sort((a, b) => b.addedAt - a.addedAt);
331
+ return [...org, ...files].map(({ addedAt: _addedAt, ...preset }) => preset);
332
+ }
333
+ /** The preset's content onto a bot that was just created: playbooks, skills
334
+ * (switched on only for an organization preset), starter notes, and the
335
+ * provenance stamp. Persona fields are the caller's: the New bot dialog
336
+ * prefilled them from the preset and the person may have edited them.
337
+ * Throws on failure; the caller removes the half-made bot. */
338
+ export function applyPresetToBot(botId, resolved, deps) {
339
+ const { row, preset } = resolved;
340
+ const org = row.source === "org";
341
+ const playbooks = (preset.playbooks ?? []).flatMap((key) => {
342
+ const playbook = resolved.playbooks.find((candidate) => candidate.key === key);
343
+ return playbook ? [{ ...playbook, triggers: [...playbook.triggers] }] : [];
344
+ });
345
+ const installed = {
346
+ id: row.packageId,
347
+ name: row.packageName,
348
+ release: row.release,
349
+ requiredApps: [],
350
+ source: row.source,
351
+ installId: row.installId,
352
+ presetKey: row.key,
353
+ ...(org && row.publisher ? { publisher: { ...row.publisher } } : {}),
354
+ ...(org && row.ref ? { ref: row.ref } : {}),
355
+ ...(org && row.sha256 ? { sha256: row.sha256 } : {}),
356
+ };
357
+ deps.store.patchBot(botId, { installedPackage: installed, ...(playbooks.length ? { playbooks } : {}) });
358
+ const skills = [];
359
+ for (const name of preset.skills ?? []) {
360
+ const skill = resolved.skills.find((candidate) => candidate.name === name);
361
+ if (!skill)
362
+ throw new Error(`The preset's skill "${name}" is unavailable`);
363
+ // The organization channel names its own source; a file's is display only.
364
+ const source = org && row.ref ? `org:${row.ref}@${row.release}` : skill.source ?? `package:${row.packageId}`;
365
+ const added = deps.skills.install(botId, source, [{ path: "SKILL.md", content: skill.instructions }]);
366
+ if ("error" in added)
367
+ throw new Error(`The preset's skill "${name}" could not be added: ${added.error}`);
368
+ if (org) {
369
+ const enabled = deps.skills.setEnabled(botId, name, true);
370
+ if ("error" in enabled)
371
+ throw new Error(`The preset's skill "${name}" could not be switched on: ${enabled.error}`);
372
+ }
373
+ skills.push(name);
374
+ }
375
+ let notes = 0;
376
+ for (const [path, value] of Object.entries(preset.seed?.memory ?? {})) {
377
+ if (path === "MEMORY.md")
378
+ deps.memory.writeIndex(botId, value);
379
+ else
380
+ deps.memory.writeTopic(botId, path.slice("memory/".length), value);
381
+ notes += 1;
382
+ }
383
+ return { skills, notes };
384
+ }
385
+ function isPortableSource(value) {
386
+ return !/^(?:[a-z]:[\\/]|[\\/]|\\\\|file:)/i.test(value);
387
+ }
388
+ /** "Include my New bot defaults as a preset": the saved defaults, reduced to
389
+ * the preset allowlist. Name, look, standing instructions, skills and (when
390
+ * `includeNotes`) starter notes. Never the model, folder, computer, approval
391
+ * level, connected apps, MCP servers, browser, peers, Chief settings or
392
+ * routines. Null when nothing is left worth sharing. `avatar` is the
393
+ * picture the dialog prepared (a data URL), checked by the exporter. */
394
+ export function presetFromDefaults(defaults, options) {
395
+ const skipped = [];
396
+ const profile = defaults?.profile ?? {};
397
+ const part = `presets[${DEFAULTS_PRESET_KEY}]`;
398
+ const trimmed = (value) => (value?.trim() ? value : undefined);
399
+ const bot = {
400
+ ...(trimmed(profile.name) ? { name: profile.name.trim() } : {}),
401
+ ...(trimmed(profile.title) ? { title: profile.title.trim() } : {}),
402
+ ...(trimmed(profile.description) ? { description: profile.description.trim() } : {}),
403
+ ...(trimmed(profile.soul) ? { soul: profile.soul } : {}),
404
+ };
405
+ const mascotExpression = profile.mascotExpression?.trim() || undefined;
406
+ const mascotBody = profile.mascotBody?.trim() || undefined;
407
+ if (profile.color || mascotExpression || mascotBody || options.avatar) {
408
+ const crop = profile.avatarCrop === "rounded" || profile.avatarCrop === "square" ? profile.avatarCrop : "circle";
409
+ bot.appearance = {
410
+ color: profile.color ?? "green",
411
+ ...(mascotExpression ? { mascotExpression } : {}),
412
+ ...(mascotBody ? { mascotBody } : {}),
413
+ ...(options.avatar ? { avatar: { ...options.avatar, crop } } : {}),
414
+ };
415
+ }
416
+ const skills = [];
417
+ for (const template of defaults?.skills ?? []) {
418
+ const parsed = parseSkillMd(template.text);
419
+ if ("error" in parsed || parsed.name !== template.name) {
420
+ skipped.push({ part: `${part}.skills[${template.name}]`, reason: "skill_changed" });
421
+ continue;
422
+ }
423
+ if (skills.length >= AGENT_MAX_SKILLS) {
424
+ skipped.push({ part: `${part}.skills[${template.name}]`, reason: "bot_skill_limit" });
425
+ continue;
426
+ }
427
+ skills.push({
428
+ name: parsed.name,
429
+ description: parsed.description,
430
+ ...(template.source && isPortableSource(template.source) ? { source: template.source } : {}),
431
+ ...(parsed.license ? { license: parsed.license } : {}),
432
+ ...(parsed.compatibility ? { compatibility: parsed.compatibility } : {}),
433
+ instructions: template.text,
434
+ enabled: template.enabled,
435
+ });
436
+ }
437
+ const memory = {};
438
+ if (options.includeNotes) {
439
+ let files = 0;
440
+ let total = 0;
441
+ for (const path of Object.keys(defaults?.memory ?? {}).sort((a, b) => (a === "MEMORY.md" ? -1 : b === "MEMORY.md" ? 1 : a.localeCompare(b)))) {
442
+ const value = defaults.memory[path];
443
+ if (!value.trim())
444
+ continue;
445
+ const bytes = utf8Bytes(value);
446
+ if (bytes > SEED_FILE_MAX_BYTES || files >= SEED_MAX_FILES || total + bytes > SEED_MAX_BYTES) {
447
+ skipped.push({ part: `${part}.seed.memory[${JSON.stringify(path)}]`, reason: "notes_too_large" });
448
+ continue;
449
+ }
450
+ memory[path] = value;
451
+ files += 1;
452
+ total += bytes;
453
+ }
454
+ }
455
+ const hasContent = Object.keys(bot).length > 0 || skills.length > 0 || Object.keys(memory).length > 0;
456
+ if (!hasContent) {
457
+ skipped.push({ part, reason: "preset_empty" });
458
+ return { value: null, skipped };
459
+ }
460
+ const name = (options.name?.trim() || profile.name?.trim() || "New bot").slice(0, 100).trim();
461
+ const description = options.description?.trim().slice(0, 300) || undefined;
462
+ return {
463
+ value: {
464
+ preset: {
465
+ key: DEFAULTS_PRESET_KEY,
466
+ name,
467
+ ...(description ? { description } : {}),
468
+ bot,
469
+ ...(skills.length ? { skills: skills.map((skill) => skill.name) } : {}),
470
+ ...(Object.keys(memory).length ? { seed: { memory } } : {}),
471
+ },
472
+ skills,
473
+ },
474
+ skipped,
475
+ };
476
+ }
477
+ // ── what this installation last shared as a preset file ────────────────────
478
+ const publishedLibrarySchema = z.object({ version: z.literal(1), packageId: z.string().regex(/^[a-z0-9][a-z0-9-]*$/).max(80), lastRelease: z.string().min(1).max(30) });
479
+ export function readPublishedLibrary(file = PUBLISHED_LIBRARY_FILE) {
480
+ try {
481
+ const parsed = publishedLibrarySchema.parse(JSON.parse(readFileSync(file, "utf8")));
482
+ return { packageId: parsed.packageId, lastRelease: parsed.lastRelease };
483
+ }
484
+ catch {
485
+ // Nothing shared yet (or unreadable): the next share starts at 1.0.0.
486
+ return null;
487
+ }
488
+ }
489
+ export function writePublishedLibrary(entry, file = PUBLISHED_LIBRARY_FILE) {
490
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
491
+ writeFileAtomic(file, JSON.stringify({ version: 1, ...entry }, null, 2), { mode: 0o600 });
492
+ }
@@ -1,8 +1,9 @@
1
- export const PROVIDER_KEY_KINDS = ["anthropic", "openaiCompat", "xai"];
1
+ export const PROVIDER_KEY_KINDS = ["anthropic", "openaiCompat", "xai", "mistral"];
2
2
  const DEFAULT_URLS = {
3
3
  anthropic: "https://api.anthropic.com",
4
4
  openaiCompat: "https://openrouter.ai/api/v1",
5
5
  xai: "https://api.x.ai/v1",
6
+ mistral: "https://api.mistral.ai/v1",
6
7
  };
7
8
  const MAX_MODELS = 5;
8
9
  function isLoopback(hostname) {
@@ -18,7 +19,7 @@ export function providerModelsUrl(provider, base) {
18
19
  }
19
20
  function modelIds(body) {
20
21
  const record = body && typeof body === "object" ? body : null;
21
- const list = Array.isArray(record?.data) ? record.data : Array.isArray(record?.models) ? record.models : [];
22
+ const list = Array.isArray(body) ? body : Array.isArray(record?.data) ? record.data : Array.isArray(record?.models) ? record.models : [];
22
23
  const ids = [];
23
24
  for (const entry of list) {
24
25
  const item = entry && typeof entry === "object" ? entry : null;
@@ -0,0 +1,80 @@
1
+ // What this installation has shared, per team: the package id and the
2
+ // release last exported, plus the key each bot, group chat and routine was
3
+ // published under.
4
+ //
5
+ // Keys are a package's identity across releases (Admin refuses a release
6
+ // that silently drops one, and later updates match on them). They must not
7
+ // come from names once they exist: renaming a bot, a room, a routine or the
8
+ // team would otherwise read as "everything removed, everything added". So
9
+ // the first export records them here, keyed by record id, and every later
10
+ // export of the same team reuses them. Teams are name strings in OMB, so the
11
+ // team entry is renamed with the section (section-context.ts calls
12
+ // renamePublishedTeam from the one function every rename goes through).
13
+ //
14
+ // Server-private: nothing here is on the wire, and nothing here is a secret.
15
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { z } from "zod";
18
+ import { writeFileAtomic } from "./atomic.js";
19
+ import { DATA_DIR } from "./config.js";
20
+ export const PUBLISHED_TEAMS_FILE = join(DATA_DIR, "published-teams.json");
21
+ const keyMap = z.record(z.string(), z.string());
22
+ const publishedTeamSchema = z.object({
23
+ packageId: z.string().min(1),
24
+ lastRelease: z.string().min(1),
25
+ keys: z.object({ bots: keyMap, rooms: keyMap, routines: keyMap }).default({ bots: {}, rooms: {}, routines: {} }),
26
+ });
27
+ const fileSchema = z.object({ version: z.literal(1), teams: z.record(z.string(), z.unknown()) });
28
+ function load() {
29
+ const teams = new Map();
30
+ if (!existsSync(PUBLISHED_TEAMS_FILE))
31
+ return teams;
32
+ try {
33
+ const file = fileSchema.parse(JSON.parse(readFileSync(PUBLISHED_TEAMS_FILE, "utf8")));
34
+ // Own entries only: a team may be called anything, including __proto__.
35
+ for (const [section, value] of Object.entries(file.teams)) {
36
+ const entry = publishedTeamSchema.safeParse(value);
37
+ if (entry.success)
38
+ teams.set(section, entry.data);
39
+ }
40
+ }
41
+ catch {
42
+ // An unreadable record only costs key stability on the next export,
43
+ // which then derives keys from names as a first export does.
44
+ }
45
+ return teams;
46
+ }
47
+ function save(teams) {
48
+ mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
49
+ const out = Object.create(null);
50
+ for (const [section, entry] of teams)
51
+ out[section] = entry;
52
+ writeFileAtomic(PUBLISHED_TEAMS_FILE, JSON.stringify({ version: 1, teams: out }, null, 2), { mode: 0o600 });
53
+ }
54
+ export function readPublishedTeam(section) {
55
+ return load().get(section) ?? null;
56
+ }
57
+ export function writePublishedTeam(section, entry) {
58
+ const teams = load();
59
+ teams.set(section, entry);
60
+ save(teams);
61
+ }
62
+ /** Follow a team rename; null forgets the team (it was deleted). */
63
+ export function renamePublishedTeam(name, nextName) {
64
+ const teams = load();
65
+ const entry = teams.get(name);
66
+ if (!entry || name === nextName)
67
+ return;
68
+ teams.delete(name);
69
+ if (nextName !== null)
70
+ teams.set(nextName, entry);
71
+ save(teams);
72
+ }
73
+ /** "1.2.3" → "1.2.4"; nothing published yet (or unreadable) starts at 1.0.0. */
74
+ export function nextPatchRelease(release) {
75
+ const match = release?.match(/^(\d{1,6})\.(\d{1,6})\.(\d{1,6})$/);
76
+ if (!match)
77
+ return "1.0.0";
78
+ const [major, minor, patch] = match.slice(1).map(Number);
79
+ return patch < 999_999 ? `${major}.${minor}.${patch + 1}` : `${major}.${minor + 1}.0`;
80
+ }
@@ -143,3 +143,18 @@ export function parseSince(value, now = Date.now()) {
143
143
  const parsed = Date.parse(/^\d{4}-\d{2}-\d{2}$/.test(text) ? `${text}T00:00` : value.trim());
144
144
  return Number.isFinite(parsed) ? parsed : null;
145
145
  }
146
+ /** The forms that name a whole day rather than one instant. */
147
+ const WHOLE_DAY = /^(?:today|yesterday|\d{4}-\d{2}-\d{2})$/;
148
+ /** The closing end of a window a person types. `since` takes a day's first
149
+ * instant, so `until` has to take its last: read the same way, `since` and
150
+ * `until` on one day span no time at all and a full day of work reads back as
151
+ * nothing. Only the forms that name a day are stretched — a span ("24h"), an
152
+ * epoch, or a date with a clock time still means the instant it names. */
153
+ export function parseUntil(value, now = Date.now()) {
154
+ const at = parseSince(value, now);
155
+ if (at === null || !WHOLE_DAY.test(value.trim().toLowerCase()))
156
+ return at;
157
+ const end = new Date(at);
158
+ end.setHours(23, 59, 59, 999);
159
+ return end.getTime();
160
+ }