thursday-agent 0.17.2 → 0.19.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 (444) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +4 -2
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/routes-manifest.json +28 -12
  6. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/server/app/_global-error.html +1 -1
  8. package/.next/server/app/_global-error.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +1 -1
  15. package/.next/server/app/_not-found.rsc +12 -12
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  17. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +11 -11
  18. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  19. package/.next/server/app/api/artifact/route.js +7 -7
  20. package/.next/server/app/api/artifact/route.js.nft.json +1 -1
  21. package/.next/server/app/api/bot/memory/files/route.js +4 -4
  22. package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
  23. package/.next/server/app/api/bot/memory/route.js +4 -4
  24. package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
  25. package/.next/server/app/api/bot/route.js +4 -4
  26. package/.next/server/app/api/bot/route.js.nft.json +1 -1
  27. package/.next/server/app/api/bot/thread/[id]/route.js +1 -1
  28. package/.next/server/app/api/bot/thread/[id]/route.js.nft.json +1 -1
  29. package/.next/server/app/api/bot/thread/file/route/app-paths-manifest.json +3 -0
  30. package/.next/server/app/api/bot/thread/file/route.js +29 -0
  31. package/.next/server/app/api/bot/thread/file/route.js.nft.json +1 -0
  32. package/.next/server/app/api/bot/thread/file/route_client-reference-manifest.js +3 -0
  33. package/.next/server/app/api/bot/thread/route.js +1 -1
  34. package/.next/server/app/api/bot/thread/route.js.nft.json +1 -1
  35. package/.next/server/app/api/config/route.js +1 -1
  36. package/.next/server/app/api/config/route.js.nft.json +1 -1
  37. package/.next/server/app/api/events/route.js +2 -2
  38. package/.next/server/app/api/events/route.js.nft.json +1 -1
  39. package/.next/server/app/api/favicon/[host]/route.js.nft.json +1 -1
  40. package/.next/server/app/api/file/[...path]/route.js +4 -4
  41. package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
  42. package/.next/server/app/api/llm-model/automatic/route.js +3 -3
  43. package/.next/server/app/api/llm-model/automatic/route.js.nft.json +1 -1
  44. package/.next/server/app/api/llm-model/catalog/[provider]/route/app-paths-manifest.json +3 -0
  45. package/.next/server/app/api/llm-model/catalog/{route.js → [provider]/route.js} +8 -8
  46. package/.next/server/app/api/llm-model/catalog/[provider]/route.js.nft.json +1 -0
  47. package/.next/server/app/api/llm-model/catalog/[provider]/route_client-reference-manifest.js +3 -0
  48. package/.next/server/app/api/llm-model/credits/[provider]/route/app-paths-manifest.json +3 -0
  49. package/.next/server/app/api/llm-model/credits/[provider]/route/build-manifest.json +12 -0
  50. package/.next/server/app/api/llm-model/credits/[provider]/route/server-reference-manifest.json +4 -0
  51. package/.next/server/app/api/llm-model/credits/{route.js → [provider]/route.js} +8 -8
  52. package/.next/server/app/api/llm-model/credits/[provider]/route.js.map +5 -0
  53. package/.next/server/app/api/llm-model/credits/[provider]/route.js.nft.json +1 -0
  54. package/.next/server/app/api/llm-model/credits/[provider]/route_client-reference-manifest.js +3 -0
  55. package/.next/server/app/api/llm-model/route.js +1 -1
  56. package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
  57. package/.next/server/app/api/llm-model/subscription/route.js +1 -1
  58. package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
  59. package/.next/server/app/api/mcp/[name]/route.js +1 -1
  60. package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
  61. package/.next/server/app/api/mcp/oauth/callback/route.js +2 -2
  62. package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
  63. package/.next/server/app/api/mcp/route.js +1 -1
  64. package/.next/server/app/api/mcp/route.js.nft.json +1 -1
  65. package/.next/server/app/api/mcp/tools/route.js +1 -1
  66. package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
  67. package/.next/server/app/api/memory/edit/route.js +12 -12
  68. package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
  69. package/.next/server/app/api/memory/route.js +1 -1
  70. package/.next/server/app/api/memory/route.js.nft.json +1 -1
  71. package/.next/server/app/api/reach/route.js +17 -17
  72. package/.next/server/app/api/reach/route.js.nft.json +1 -1
  73. package/.next/server/app/api/routine/route.js +6 -6
  74. package/.next/server/app/api/routine/route.js.nft.json +1 -1
  75. package/.next/server/app/api/running/route/app-paths-manifest.json +3 -0
  76. package/.next/server/app/api/running/route/build-manifest.json +12 -0
  77. package/.next/server/app/api/running/route/server-reference-manifest.json +4 -0
  78. package/.next/server/app/api/running/route.js +10 -0
  79. package/.next/server/app/api/running/route.js.map +5 -0
  80. package/.next/server/app/api/running/route.js.nft.json +1 -0
  81. package/.next/server/app/api/running/route_client-reference-manifest.js +3 -0
  82. package/.next/server/app/api/signins/route.js +4 -4
  83. package/.next/server/app/api/signins/route.js.nft.json +1 -1
  84. package/.next/server/app/api/skills/[source]/[name]/route.js +7 -7
  85. package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
  86. package/.next/server/app/api/skills/route.js +6 -6
  87. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  88. package/.next/server/app/api/thursday/call/end/route.js +1 -1
  89. package/.next/server/app/api/thursday/call/end/route.js.nft.json +1 -1
  90. package/.next/server/app/api/thursday/call/route.js +1 -1
  91. package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
  92. package/.next/server/app/api/thursday/settings/route.js +1 -1
  93. package/.next/server/app/api/thursday/settings/route.js.nft.json +1 -1
  94. package/.next/server/app/api/thursday/text/route.js +12 -12
  95. package/.next/server/app/api/thursday/text/route.js.nft.json +1 -1
  96. package/.next/server/app/api/thursday/tool-call/route.js +13 -13
  97. package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
  98. package/.next/server/app/api/workspace/files/route.js +7 -7
  99. package/.next/server/app/api/workspace/files/route.js.nft.json +1 -1
  100. package/.next/server/app/api/workspace/route.js +6 -6
  101. package/.next/server/app/api/workspace/route.js.nft.json +1 -1
  102. package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +94 -6
  103. package/.next/server/app/artifact/[...path]/page.js +9 -5
  104. package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
  105. package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
  106. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  107. package/.next/server/app/manifest.webmanifest/route.js.nft.json +1 -1
  108. package/.next/server/app/page/server-reference-manifest.json +161 -128
  109. package/.next/server/app/page.js +10 -9
  110. package/.next/server/app/page.js.nft.json +1 -1
  111. package/.next/server/app/page_client-reference-manifest.js +1 -1
  112. package/.next/server/app-paths-manifest.json +4 -2
  113. package/.next/server/chunks/[root-of-the-server]__00qyw01._.js +1 -1
  114. package/.next/server/chunks/[root-of-the-server]__02ra64t._.js +5 -0
  115. package/.next/server/chunks/{[root-of-the-server]__05dp5tr._.js → [root-of-the-server]__06ky9of._.js} +2 -2
  116. package/.next/server/chunks/[root-of-the-server]__0b565-x._.js +7 -0
  117. package/.next/server/chunks/[root-of-the-server]__0bmtib_._.js +7 -0
  118. package/.next/server/chunks/[root-of-the-server]__0bqveex._.js +1 -1
  119. package/.next/server/chunks/[root-of-the-server]__0brmiv2._.js +7 -0
  120. package/.next/server/chunks/[root-of-the-server]__0eivkvk._.js +2 -2
  121. package/.next/server/chunks/[root-of-the-server]__0f7m79q._.js +1 -1
  122. package/.next/server/chunks/[root-of-the-server]__0g38-aj._.js +2 -2
  123. package/.next/server/chunks/[root-of-the-server]__0gvdr2h._.js +5 -0
  124. package/.next/server/chunks/[root-of-the-server]__0h656y-._.js +2 -2
  125. package/.next/server/chunks/[root-of-the-server]__0hm3tiq._.js +2 -2
  126. package/.next/server/chunks/[root-of-the-server]__0hzdqnv._.js +2 -2
  127. package/.next/server/chunks/[root-of-the-server]__0kzmxpg._.js +3 -0
  128. package/.next/server/chunks/[root-of-the-server]__0mgyt8p._.js +2 -2
  129. package/.next/server/chunks/[root-of-the-server]__0mrk2it._.js +1 -1
  130. package/.next/server/chunks/[root-of-the-server]__0o5s5-o._.js +5 -0
  131. package/.next/server/chunks/[root-of-the-server]__0p80i1n._.js +1 -1
  132. package/.next/server/chunks/[root-of-the-server]__0qdoc_9._.js +5 -0
  133. package/.next/server/chunks/[root-of-the-server]__0qzho69._.js +1 -1
  134. package/.next/server/chunks/[root-of-the-server]__0sm5xvm._.js +1 -1
  135. package/.next/server/chunks/[root-of-the-server]__0v_kuor._.js +5 -0
  136. package/.next/server/chunks/{[root-of-the-server]__061o2_x._.js → [root-of-the-server]__0ypkfbu._.js} +1 -1
  137. package/.next/server/chunks/[root-of-the-server]__1-18_pz._.js +1 -1
  138. package/.next/server/chunks/[root-of-the-server]__13hng6h._.js +1 -1
  139. package/.next/server/chunks/{[root-of-the-server]__01k3vi3._.js → [root-of-the-server]__14iga7p._.js} +1 -1
  140. package/.next/server/chunks/[root-of-the-server]__16gn7zn._.js +3 -0
  141. package/.next/server/chunks/[root-of-the-server]__18e65_-._.js +3 -0
  142. package/.next/server/chunks/[root-of-the-server]__1_04mcf._.js +2 -2
  143. package/.next/server/chunks/[root-of-the-server]__1_cgawk._.js +5 -0
  144. package/.next/server/chunks/[root-of-the-server]__1ceammd._.js +1 -1
  145. package/.next/server/chunks/[root-of-the-server]__1ckt1yj._.js +1 -1
  146. package/.next/server/chunks/[root-of-the-server]__1cu5f3z._.js +1 -1
  147. package/.next/server/chunks/[root-of-the-server]__1dth047._.js +5 -0
  148. package/.next/server/chunks/{[root-of-the-server]__1k2qs1m._.js → [root-of-the-server]__1fc6v12._.js} +1 -1
  149. package/.next/server/chunks/[root-of-the-server]__1hviq62._.js +2 -2
  150. package/.next/server/chunks/[root-of-the-server]__1ksm9m-._.js +12 -0
  151. package/.next/server/chunks/[root-of-the-server]__1m1q6u5._.js +3 -0
  152. package/.next/server/chunks/[root-of-the-server]__1m_dmf6._.js +5 -0
  153. package/.next/server/chunks/[root-of-the-server]__1mys2c3._.js +5 -0
  154. package/.next/server/chunks/[root-of-the-server]__1ndi14y._.js +1 -1
  155. package/.next/server/chunks/[root-of-the-server]__1p0njle._.js +5 -0
  156. package/.next/server/chunks/[root-of-the-server]__1p47fbb._.js +2 -2
  157. package/.next/server/chunks/[root-of-the-server]__1pugoq0._.js +1 -1
  158. package/.next/server/chunks/{[root-of-the-server]__0ts7ai0._.js → [root-of-the-server]__1trns9g._.js} +1 -1
  159. package/.next/server/chunks/[root-of-the-server]__1ygplx1._.js +1 -1
  160. package/.next/server/chunks/[root-of-the-server]__1zn039k._.js +1 -1
  161. package/.next/server/chunks/[root-of-the-server]__20w_g71._.js +3 -0
  162. package/.next/server/chunks/_01c32w9._.js +1 -1
  163. package/.next/server/chunks/_02bov6_._.js +1 -1
  164. package/.next/server/chunks/_06hnfi8._.js +2 -2
  165. package/.next/server/chunks/_06u9yo-._.js +1 -1
  166. package/.next/server/chunks/_08354bn._.js +12 -11
  167. package/.next/server/chunks/_08_-9ay._.js +7 -0
  168. package/.next/server/chunks/_08s4vft._.js +1 -1
  169. package/.next/server/chunks/_0bgjf07._.js +5 -0
  170. package/.next/server/chunks/_0c49lmw._.js +1 -1
  171. package/.next/server/chunks/_0fb6dvr._.js +1 -1
  172. package/.next/server/chunks/_0hgvg55._.js +1 -1
  173. package/.next/server/chunks/_0iw19ig._.js +1 -1
  174. package/.next/server/chunks/_0poi1t2._.js +1 -1
  175. package/.next/server/chunks/_0s8ao8z._.js +1 -1
  176. package/.next/server/chunks/_0t_7gji._.js +1 -1
  177. package/.next/server/chunks/_0ucx1jv._.js +1 -1
  178. package/.next/server/chunks/_0uzj57i._.js +1 -1
  179. package/.next/server/chunks/_0wyh244._.js +112 -0
  180. package/.next/server/chunks/_1-sk_q4._.js +1 -1
  181. package/.next/server/chunks/_108gsm8._.js +2 -2
  182. package/.next/server/chunks/_136j09a._.js +5 -5
  183. package/.next/server/chunks/_15negpy._.js +2 -2
  184. package/.next/server/chunks/_1d7og_q._.js +1 -1
  185. package/.next/server/chunks/_1dxq827._.js +2 -2
  186. package/.next/server/chunks/_1e54wxa._.js +1 -1
  187. package/.next/server/chunks/_1frcwj1._.js +1 -1
  188. package/.next/server/chunks/_1gn2r2q._.js +8 -6
  189. package/.next/server/chunks/_1gxankq._.js +2 -2
  190. package/.next/server/chunks/_1i3uucm._.js +1 -1
  191. package/.next/server/chunks/_1jvy78-._.js +5 -0
  192. package/.next/server/chunks/_1lg0lsu._.js +53 -0
  193. package/.next/server/chunks/_1njolhx._.js +294 -0
  194. package/.next/server/chunks/_1q6e56i._.js +8 -6
  195. package/.next/server/chunks/_1qgix-j._.js +1 -1
  196. package/.next/server/chunks/_1wtb-77._.js +294 -0
  197. package/.next/server/chunks/_1wwl_7o._.js +1 -1
  198. package/.next/server/chunks/_1y8w80a._.js +5 -5
  199. package/.next/server/chunks/_1yx285_._.js +3 -0
  200. package/.next/server/chunks/_20qn224._.js +3 -0
  201. package/.next/server/chunks/_211dn2c._.js +1 -1
  202. package/.next/server/chunks/_next-internal_server_app_api_bot_thread_file_route_actions_0q3be0o.js +3 -0
  203. package/.next/server/chunks/_next-internal_server_app_api_llm-model_catalog_[provider]_route_actions_1i0449q.js +3 -0
  204. package/.next/server/chunks/_next-internal_server_app_api_llm-model_credits_[provider]_route_actions_0iui9hz.js +3 -0
  205. package/.next/server/chunks/_next-internal_server_app_api_running_route_actions_0o1g-aw.js +3 -0
  206. package/.next/server/chunks/config_ts_1g4sz5o._.js +1 -1
  207. package/.next/server/chunks/features_bot_08z387_._.js +3 -3
  208. package/.next/server/chunks/features_bot_0zy_q8y._.js +7 -7
  209. package/.next/server/chunks/features_bot_1dfrdsb._.js +7 -7
  210. package/.next/server/chunks/features_workspace_workspace_ts_13h82gv._.js +3 -3
  211. package/.next/server/chunks/features_workspace_workspace_ts_1iyjroj._.js +3 -3
  212. package/.next/server/chunks/{node_modules_1rosz79._.js → node_modules_035o7ku._.js} +1 -1
  213. package/.next/server/chunks/{node_modules_1er1u8w._.js → node_modules_0tn5t13._.js} +1 -1
  214. package/.next/server/chunks/{node_modules_103lpef._.js → node_modules_12oq3hr._.js} +1 -1
  215. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0g87zst._.js → node_modules_consola_dist_chunks_prompt_mjs_0yd5xa0._.js} +1 -1
  216. package/.next/server/chunks/ssr/[root-of-the-server]__04upd5v._.js +1 -1
  217. package/.next/server/chunks/ssr/[root-of-the-server]__0p0mv2u._.js +3 -0
  218. package/.next/server/chunks/ssr/[root-of-the-server]__0sk5enu._.js +3 -0
  219. package/.next/server/chunks/ssr/[root-of-the-server]__0xyaud4._.js +3 -0
  220. package/.next/server/chunks/ssr/[root-of-the-server]__1-gi9sm._.js +3 -0
  221. package/.next/server/chunks/ssr/[root-of-the-server]__16nn3pe._.js +3 -0
  222. package/.next/server/chunks/ssr/[root-of-the-server]__1867koj._.js +1 -1
  223. package/.next/server/chunks/ssr/[root-of-the-server]__1c8-7cw._.js +3 -0
  224. package/.next/server/chunks/ssr/{[root-of-the-server]__1d7usu0._.js → [root-of-the-server]__1qn0axo._.js} +2 -2
  225. package/.next/server/chunks/ssr/[root-of-the-server]__1tb5vve._.js +47 -0
  226. package/.next/server/chunks/ssr/_02cm0fr._.js +5 -0
  227. package/.next/server/chunks/ssr/{features_bot_1knyan7._.js → _07nmtxo._.js} +2 -2
  228. package/.next/server/chunks/ssr/_088vzgn._.js +1 -1
  229. package/.next/server/chunks/ssr/{_1dfv66y._.js → _0bg1u1j._.js} +2 -2
  230. package/.next/server/chunks/ssr/_0cv9tmv._.js +1 -1
  231. package/.next/server/chunks/ssr/{_10-_awu._.js → _0f9hx5p._.js} +2 -2
  232. package/.next/server/chunks/ssr/_0g9ula_._.js +3 -0
  233. package/.next/server/chunks/ssr/{_1aqjk-1._.js → _0irozu9._.js} +2 -2
  234. package/.next/server/chunks/ssr/{_123y-ah._.js → _0is8m6y._.js} +2 -2
  235. package/.next/server/chunks/ssr/_0iwsdp7._.js +3 -0
  236. package/.next/server/chunks/ssr/_0l7rjg5._.js +1 -1
  237. package/.next/server/chunks/ssr/{lib_logger_ts_0-olsze._.js → _0mfgsk-._.js} +4 -2
  238. package/.next/server/chunks/ssr/{_0fo4552._.js → _0nk80t6._.js} +53 -13
  239. package/.next/server/chunks/ssr/_0p6w0ef._.js +3 -0
  240. package/.next/server/chunks/ssr/_0pbav_w._.js +3 -0
  241. package/.next/server/chunks/ssr/{_0vo_oym._.js → _0q-mdwv._.js} +8 -8
  242. package/.next/server/chunks/ssr/_0s5_ahb._.js +3 -0
  243. package/.next/server/chunks/ssr/{_0e2fnvg._.js → _0t5f2c3._.js} +2 -2
  244. package/.next/server/chunks/ssr/_0v9r8g2._.js +1 -1
  245. package/.next/server/chunks/ssr/_0x8ophf._.js +1 -1
  246. package/.next/server/chunks/ssr/_0x9f717._.js +60 -0
  247. package/.next/server/chunks/ssr/_107vtdk._.js +162 -0
  248. package/.next/server/chunks/ssr/{_17a4qyk._.js → _12lql1x._.js} +3 -3
  249. package/.next/server/chunks/ssr/{_163dr78._.js → _165wnr4._.js} +2 -2
  250. package/.next/server/chunks/ssr/_16_7zeo._.js +3 -0
  251. package/.next/server/chunks/ssr/_17komoi._.js +3 -0
  252. package/.next/server/chunks/ssr/_190hjys._.js +3 -0
  253. package/.next/server/chunks/ssr/_1bfrntd._.js +6 -0
  254. package/.next/server/chunks/ssr/{_01al00i._.js → _1booff4._.js} +2 -2
  255. package/.next/server/chunks/ssr/{_0xfafez._.js → _1ew1j7l._.js} +2 -2
  256. package/.next/server/chunks/ssr/_1i2swyr._.js +3 -0
  257. package/.next/server/chunks/ssr/_1neoj22._.js +1 -1
  258. package/.next/server/chunks/ssr/_1qsa25k._.js +3 -0
  259. package/.next/server/chunks/ssr/_1soynbw._.js +6 -0
  260. package/.next/server/chunks/ssr/_1vc2xmu._.js +3 -0
  261. package/.next/server/chunks/ssr/{_14oirtb._.js → _1wq90j8._.js} +2 -2
  262. package/.next/server/chunks/ssr/_1xwtbdz._.js +3 -0
  263. package/.next/server/chunks/ssr/_1zrhkf0._.js +310 -0
  264. package/.next/server/chunks/ssr/_21awrs2._.js +5 -0
  265. package/.next/server/chunks/ssr/features_0796o8r._.js +1 -1
  266. package/.next/server/chunks/ssr/features_bot_0tb4ui-._.js +2 -2
  267. package/.next/server/chunks/ssr/features_bot_1c56lew._.js +37 -0
  268. package/.next/server/chunks/ssr/features_bot_1fn_grf._.js +3 -0
  269. package/.next/server/chunks/ssr/{features_config_09n-q9q._.js → features_config_05zi68v._.js} +3 -3
  270. package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
  271. package/.next/server/chunks/ssr/features_memory_components_memory-setting_tsx_0oa43sw._.js +1 -1
  272. package/.next/server/chunks/ssr/features_routine_components_routine-setting_tsx_055fknz._.js +1 -1
  273. package/.next/server/chunks/ssr/features_workspace_workspace_ts_1wc7_u_._.js +4 -4
  274. package/.next/server/chunks/ssr/{node_modules_1k5i-_9._.js → node_modules_0-sux1a._.js} +1 -1
  275. package/.next/server/chunks/ssr/{node_modules_0msrhxf._.js → node_modules_01oezhb._.js} +1 -1
  276. package/.next/server/chunks/ssr/{node_modules_1gq3ght._.js → node_modules_04m6ebg._.js} +1 -1
  277. package/.next/server/chunks/ssr/{node_modules_0nmkv-v._.js → node_modules_067jfov._.js} +1 -1
  278. package/.next/server/chunks/ssr/{node_modules_0gd9hbz._.js → node_modules_06ugby3._.js} +1 -1
  279. package/.next/server/chunks/ssr/{node_modules_0d6j33x._.js → node_modules_07p51j3._.js} +1 -1
  280. package/.next/server/chunks/ssr/{node_modules_10_fv2a._.js → node_modules_0995xyq._.js} +1 -1
  281. package/.next/server/chunks/ssr/{node_modules_1lmyts8._.js → node_modules_0_uecwx._.js} +1 -1
  282. package/.next/server/chunks/ssr/{node_modules_04x5wyv._.js → node_modules_0dsyc90._.js} +1 -1
  283. package/.next/server/chunks/ssr/{node_modules_0__0tvm._.js → node_modules_0hli6s2._.js} +1 -1
  284. package/.next/server/chunks/ssr/{node_modules_0k8-y89._.js → node_modules_0m-bde_._.js} +1 -1
  285. package/.next/server/chunks/ssr/{node_modules_1p_6sq7._.js → node_modules_0oqfdbr._.js} +1 -1
  286. package/.next/server/chunks/ssr/node_modules_0qd7p5f._.js +3 -0
  287. package/.next/server/chunks/ssr/{node_modules_13g-d25._.js → node_modules_0tgy5s5._.js} +1 -1
  288. package/.next/server/chunks/ssr/{node_modules_1_8-jcj._.js → node_modules_1-kjmls._.js} +1 -1
  289. package/.next/server/chunks/ssr/{node_modules_0m73b88._.js → node_modules_1-l3ka5._.js} +1 -1
  290. package/.next/server/chunks/ssr/{node_modules_113l3e5._.js → node_modules_1-w3ayh._.js} +1 -1
  291. package/.next/server/chunks/ssr/{node_modules_0q-klni._.js → node_modules_133exgm._.js} +1 -1
  292. package/.next/server/chunks/ssr/{node_modules_1xb-ndg._.js → node_modules_17xc5_k._.js} +1 -1
  293. package/.next/server/chunks/ssr/{node_modules_021lid9._.js → node_modules_1_i_w30._.js} +1 -1
  294. package/.next/server/chunks/ssr/{node_modules_1ri_eip._.js → node_modules_1a2cjfk._.js} +1 -1
  295. package/.next/server/chunks/ssr/{node_modules_13bqv6i._.js → node_modules_1k752fe._.js} +1 -1
  296. package/.next/server/chunks/ssr/{node_modules_1qstb1x._.js → node_modules_1lie7z8._.js} +1 -1
  297. package/.next/server/chunks/ssr/{node_modules_096up6a._.js → node_modules_1mv70-y._.js} +1 -1
  298. package/.next/server/chunks/ssr/{node_modules_156kv_t._.js → node_modules_1r611t2._.js} +1 -1
  299. package/.next/server/chunks/ssr/node_modules_1vfd8dz._.js +3 -0
  300. package/.next/server/chunks/ssr/{node_modules_0cjnmzp._.js → node_modules_1yaowd7._.js} +1 -1
  301. package/.next/server/chunks/ssr/{node_modules_0nw5jz7._.js → node_modules_20a1wpo._.js} +1 -1
  302. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0amkw0b._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1r8pxbe._.js} +1 -1
  303. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0zq3l45._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1siiu4h._.js} +1 -1
  304. package/.next/server/middleware-build-manifest.js +3 -3
  305. package/.next/server/pages/404.html +1 -1
  306. package/.next/server/pages/500.html +1 -1
  307. package/.next/server/server-reference-manifest.js +1 -1
  308. package/.next/server/server-reference-manifest.json +205 -132
  309. package/.next/static/chunks/{0obogzscwwekj.js → 088d3w5qxu19g.js} +2 -2
  310. package/.next/static/chunks/0_s4z70lx1tgs.js +1 -0
  311. package/.next/static/chunks/0bxekp4hnuk1g.js +1 -0
  312. package/.next/static/chunks/{1cglo3a2mbeou.js → 0fw4u_s974j24.js} +1 -1
  313. package/.next/static/chunks/0hy2x7qhk8wgx.css +1 -0
  314. package/.next/static/chunks/{2zbl41hed17g3.js → 0n_eshmn_7wfn.js} +2 -2
  315. package/.next/static/chunks/0q9b2n6l4flqb.js +1 -0
  316. package/.next/static/chunks/0ulcp_u_y1u0b.js +15 -0
  317. package/.next/static/chunks/0vj93zo0yvix9.js +35 -0
  318. package/.next/static/chunks/{166smhw8v-s3j.js → 10f98bh4hdmhe.js} +1 -1
  319. package/.next/static/chunks/1b8bce_z_act0.js +1 -0
  320. package/.next/static/chunks/1c32d2azsjaj_.js +1 -0
  321. package/.next/static/chunks/{1xv4g10m-cl1s.js → 1fs829mz69pxz.js} +1 -1
  322. package/.next/static/chunks/1lwlh4mo_vv8d.js +1 -0
  323. package/.next/static/chunks/{0p2f6uv4xytfm.js → 1po8fkg40peu-.js} +1 -1
  324. package/.next/static/chunks/{0uxq_vs6uxi-b.js → 1rg6b9l5fm1hj.js} +1 -1
  325. package/.next/static/chunks/{3b96xy59nl_gd.js → 26g3iwfuad-7p.js} +1 -1
  326. package/.next/static/chunks/2bjnmtke_q43c.js +1 -0
  327. package/.next/static/chunks/2c86k0o-1p9_w.js +1 -0
  328. package/.next/static/chunks/2gf4t0d76bvyv.js +1 -0
  329. package/.next/static/chunks/2rj3q_ahctv19.js +1 -0
  330. package/.next/static/chunks/{2lhc74urf3acr.js → 2x6-fpctvucte.js} +1 -1
  331. package/.next/static/chunks/2yicf7bky866n.js +3 -0
  332. package/.next/static/chunks/337g9g_ux2gj-.js +1 -0
  333. package/.next/static/chunks/350ws7svim7ij.js +58 -0
  334. package/.next/static/chunks/{0o7o0eb7ihxi6.js → 396enq-m0sss5.js} +1 -1
  335. package/.next/static/chunks/3e1w_nwfb39j-.js +1 -0
  336. package/.next/static/chunks/3v3ooretb54ag.js +1 -0
  337. package/.next/static/chunks/40re_3ukjulg9.js +1 -0
  338. package/.next/static/chunks/{3gsxnqqqphh-1.js → 44bcbnmup71so.js} +1 -1
  339. package/.next/static/chunks/{2qgkbdshzpq1c.js → 458dtbsemx996.js} +1 -1
  340. package/README.md +5 -5
  341. package/bin/background.mjs +995 -0
  342. package/bin/lock.mjs +121 -5
  343. package/bin/thursday.mjs +126 -30
  344. package/bin/tools.mjs +72 -1
  345. package/database/migrations/20260926021847_thread_cache_tokens/migration.sql +2 -0
  346. package/database/migrations/20260926021847_thread_cache_tokens/snapshot.json +1949 -0
  347. package/guide/bots.md +173 -317
  348. package/guide/calls.md +106 -146
  349. package/guide/index.md +2 -2
  350. package/guide/memory.md +14 -18
  351. package/guide/phone.md +86 -133
  352. package/guide/setup.md +141 -172
  353. package/guide/trouble.md +42 -65
  354. package/package.json +1 -1
  355. package/seed-skills/concierge/travel/references/itinerary.md +1 -1
  356. package/seed-skills/concierge/travel/scripts/itinerary.mjs +8 -3
  357. package/skills/artifact/references/book.md +3 -3
  358. package/skills/artifact/runtime/shell/shell.js +32 -3
  359. package/.next/server/app/api/llm-model/catalog/route/app-paths-manifest.json +0 -3
  360. package/.next/server/app/api/llm-model/catalog/route.js.nft.json +0 -1
  361. package/.next/server/app/api/llm-model/catalog/route_client-reference-manifest.js +0 -3
  362. package/.next/server/app/api/llm-model/credits/route/app-paths-manifest.json +0 -3
  363. package/.next/server/app/api/llm-model/credits/route.js.nft.json +0 -1
  364. package/.next/server/app/api/llm-model/credits/route_client-reference-manifest.js +0 -3
  365. package/.next/server/chunks/[root-of-the-server]__01ypm_z._.js +0 -3
  366. package/.next/server/chunks/[root-of-the-server]__021dkui._.js +0 -3
  367. package/.next/server/chunks/[root-of-the-server]__03xamry._.js +0 -3
  368. package/.next/server/chunks/[root-of-the-server]__09gdscj._.js +0 -7
  369. package/.next/server/chunks/[root-of-the-server]__0ljizgj._.js +0 -5
  370. package/.next/server/chunks/[root-of-the-server]__0nmust6._.js +0 -5
  371. package/.next/server/chunks/[root-of-the-server]__0svzjwa._.js +0 -7
  372. package/.next/server/chunks/[root-of-the-server]__1-rmal0._.js +0 -5
  373. package/.next/server/chunks/[root-of-the-server]__10xnb7e._.js +0 -3
  374. package/.next/server/chunks/[root-of-the-server]__1_h4hf9._.js +0 -7
  375. package/.next/server/chunks/[root-of-the-server]__1c2kwku._.js +0 -12
  376. package/.next/server/chunks/[root-of-the-server]__1t02-1p._.js +0 -3
  377. package/.next/server/chunks/[root-of-the-server]__1t0jj1j._.js +0 -5
  378. package/.next/server/chunks/[root-of-the-server]__1v7x5ym._.js +0 -3
  379. package/.next/server/chunks/[root-of-the-server]__1vlxv19._.js +0 -5
  380. package/.next/server/chunks/_07nxi75._.js +0 -254
  381. package/.next/server/chunks/_08btiir._.js +0 -7
  382. package/.next/server/chunks/_0j0-gq6._.js +0 -5
  383. package/.next/server/chunks/_0p6v1fr._.js +0 -5
  384. package/.next/server/chunks/_0srlnvd._.js +0 -16
  385. package/.next/server/chunks/_0xygcxs._.js +0 -254
  386. package/.next/server/chunks/_1ic3bew._.js +0 -5
  387. package/.next/server/chunks/_next-internal_server_app_api_llm-model_catalog_route_actions_1eu2y41.js +0 -3
  388. package/.next/server/chunks/_next-internal_server_app_api_llm-model_credits_route_actions_0v424pb.js +0 -3
  389. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_04oxl25._.js +0 -3
  390. package/.next/server/chunks/ssr/[root-of-the-server]__0-af_o7._.js +0 -3
  391. package/.next/server/chunks/ssr/[root-of-the-server]__09f39cp._.js +0 -4
  392. package/.next/server/chunks/ssr/[root-of-the-server]__0ouu4qp._.js +0 -3
  393. package/.next/server/chunks/ssr/[root-of-the-server]__0q9g9ea._.js +0 -5
  394. package/.next/server/chunks/ssr/[root-of-the-server]__17h1ggo._.js +0 -46
  395. package/.next/server/chunks/ssr/[root-of-the-server]__1jyeu61._.js +0 -5
  396. package/.next/server/chunks/ssr/_0afrpci._.js +0 -3
  397. package/.next/server/chunks/ssr/_0bgse1c._.js +0 -162
  398. package/.next/server/chunks/ssr/_0jstvlg._.js +0 -39
  399. package/.next/server/chunks/ssr/_0qsmpcu._.js +0 -3
  400. package/.next/server/chunks/ssr/_0z38nha._.js +0 -3
  401. package/.next/server/chunks/ssr/_1-41lck._.js +0 -3
  402. package/.next/server/chunks/ssr/_10_ph1t._.js +0 -3
  403. package/.next/server/chunks/ssr/_116lfdc._.js +0 -3
  404. package/.next/server/chunks/ssr/_14dfh7a._.js +0 -4
  405. package/.next/server/chunks/ssr/_17qtuv7._.js +0 -3
  406. package/.next/server/chunks/ssr/_19jez1a._.js +0 -3
  407. package/.next/server/chunks/ssr/_1_5xof_._.js +0 -3
  408. package/.next/server/chunks/ssr/_1alxym6._.js +0 -3
  409. package/.next/server/chunks/ssr/_1ou5sj0._.js +0 -3
  410. package/.next/server/chunks/ssr/_1tmw842._.js +0 -5
  411. package/.next/server/chunks/ssr/_1uij_rj._.js +0 -3
  412. package/.next/server/chunks/ssr/_1w9w1ok._.js +0 -3
  413. package/.next/server/chunks/ssr/_20gq_24._.js +0 -3
  414. package/.next/server/chunks/ssr/features_bot_0ouemw8._.js +0 -3
  415. package/.next/server/chunks/ssr/node_modules_@base-ui_react_1j9i1ho._.js +0 -3
  416. package/.next/static/chunks/01l7kw74vfp32.js +0 -1
  417. package/.next/static/chunks/0a5_-jqhaktjj.js +0 -1
  418. package/.next/static/chunks/0npvak0wpgagb.js +0 -1
  419. package/.next/static/chunks/19l81deo_pyoc.js +0 -1
  420. package/.next/static/chunks/1bnhw-5fj7aiz.js +0 -1
  421. package/.next/static/chunks/1in6nxpu33iab.js +0 -1
  422. package/.next/static/chunks/1u7kg91f9qy3g.js +0 -1
  423. package/.next/static/chunks/2ajs12-fcdwzm.js +0 -1
  424. package/.next/static/chunks/2i7qtmxc1kuqg.js +0 -1
  425. package/.next/static/chunks/2kz1f6r59nky0.js +0 -3
  426. package/.next/static/chunks/31kn4_h2rvf6k.css +0 -1
  427. package/.next/static/chunks/35jj-5u4te56r.js +0 -1
  428. package/.next/static/chunks/37ixzapa06xlo.js +0 -1
  429. package/.next/static/chunks/3jtcieuk1l6tw.js +0 -1
  430. package/.next/static/chunks/3k1d8y03t5nax.js +0 -58
  431. package/.next/static/chunks/3sla-dm1puqd1.js +0 -35
  432. package/.next/static/chunks/3t2c76awm9k_p.js +0 -1
  433. package/.next/static/chunks/3vrmcm31ih7gb.js +0 -1
  434. package/.next/static/chunks/3yiqr95d7ybeq.js +0 -15
  435. package/bin/autostart.mjs +0 -162
  436. /package/.next/server/app/api/{llm-model/catalog → bot/thread/file}/route/build-manifest.json +0 -0
  437. /package/.next/server/app/api/{llm-model/catalog → bot/thread/file}/route/server-reference-manifest.json +0 -0
  438. /package/.next/server/app/api/{llm-model/catalog → bot/thread/file}/route.js.map +0 -0
  439. /package/.next/server/app/api/llm-model/{credits → catalog/[provider]}/route/build-manifest.json +0 -0
  440. /package/.next/server/app/api/llm-model/{credits → catalog/[provider]}/route/server-reference-manifest.json +0 -0
  441. /package/.next/server/app/api/llm-model/{credits → catalog/[provider]}/route.js.map +0 -0
  442. /package/.next/static/{8I2hKtciyA9LXvd8txuOz → YhaRHJqObuAcXZe5VLFs0}/_buildManifest.js +0 -0
  443. /package/.next/static/{8I2hKtciyA9LXvd8txuOz → YhaRHJqObuAcXZe5VLFs0}/_clientMiddlewareManifest.js +0 -0
  444. /package/.next/static/{8I2hKtciyA9LXvd8txuOz → YhaRHJqObuAcXZe5VLFs0}/_ssgManifest.js +0 -0
@@ -0,0 +1,995 @@
1
+ // Running in the background: macOS's launchd starts the server when the person logs in, starts
2
+ // it again if it stops, and no terminal is needed. `start`, `stop` and `status`, and the one
3
+ // question a first run in a terminal asks. Plain JavaScript and no app imports, like the rest
4
+ // of bin: it runs from the published package.
5
+
6
+ import { spawnSync } from "node:child_process";
7
+ import {
8
+ closeSync,
9
+ copyFileSync,
10
+ existsSync,
11
+ fstatSync,
12
+ mkdirSync,
13
+ openSync,
14
+ readdirSync,
15
+ readFileSync,
16
+ readSync,
17
+ realpathSync,
18
+ renameSync,
19
+ rmSync,
20
+ statSync,
21
+ truncateSync,
22
+ writeFileSync,
23
+ } from "node:fs";
24
+ import { homedir, tmpdir } from "node:os";
25
+ import { delimiter, dirname, join, sep } from "node:path";
26
+ import { createInterface } from "node:readline/promises";
27
+ import { heldBy, runningOn, startedAt, stillRuns, stopLines } from "./lock.mjs";
28
+ import { homePort, portTaken } from "./port.mjs";
29
+ import {
30
+ commandFor,
31
+ isCheckout,
32
+ openBrowser,
33
+ PROGRAM,
34
+ thursdayCommand,
35
+ } from "./tools.mjs";
36
+
37
+ /** launchd's name for the job. The file it reads is named after it. */
38
+ const LABEL = "thursday-agent";
39
+
40
+ /** Named as private files are here, so a checkout never commits its own log. */
41
+ const LOG_FILE = "server.local.log";
42
+
43
+ /**
44
+ * The size at which the log starts over. The one before is kept beside it as `.1`, replacing
45
+ * the one before that, so the two never hold more than twice this. Larger keeps more of a long
46
+ * run; smaller loses its start sooner.
47
+ */
48
+ const LOG_BYTES = 10 * 1024 * 1024;
49
+
50
+ /** How often the server running in the background looks at its log's size. */
51
+ const LOG_CHECK_MS = 10 * 60_000;
52
+
53
+ /**
54
+ * Written when the person has decided about the background — answered no, or stopped it — so a
55
+ * run in a terminal does not ask again.
56
+ */
57
+ const DECIDED_FILE = "background.local.txt";
58
+
59
+ /** How long the port gets to come free after the job holding it is stopped. */
60
+ const FREE_MS = 10_000;
61
+
62
+ /** How long a job just started gets to answer: a first boot migrates the database. */
63
+ const UP_MS = 90_000;
64
+
65
+ const AGENTS = join(homedir(), "Library", "LaunchAgents");
66
+ const PLIST = join(AGENTS, `${LABEL}.plist`);
67
+
68
+ /** Written into a copy's folder under PROGRAM once npm has put all of it there. */
69
+ const INSTALLED = "installed.json";
70
+
71
+ /**
72
+ * One start at a time: two would install into one folder, and each replace the other's job. In
73
+ * the temporary folder, which is this person's own on a Mac, so it holds nothing for long.
74
+ */
75
+ const START_LOCK = join(tmpdir(), `${LABEL}-start.lock`);
76
+
77
+ /** This login session. A user agent, so it goes with the person, not the machine. */
78
+ const domain = () => `gui/${process.getuid()}`;
79
+
80
+ const launchctl = (...args) =>
81
+ spawnSync("launchctl", args, { encoding: "utf8" });
82
+
83
+ /** A path holding `&` or `<` would end the document early. */
84
+ const xml = (text) =>
85
+ text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
86
+ const unxml = (text) =>
87
+ text.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
88
+
89
+ const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
90
+
91
+ const mac = () => process.platform === "darwin";
92
+
93
+ /**
94
+ * What the job is set to run, read back from its file: the data folder and the port. Null when
95
+ * there is no job, or on another system. One job per login: it serves one data folder.
96
+ */
97
+ export function backgroundJob() {
98
+ if (!mac()) return null;
99
+ let plist;
100
+ try {
101
+ plist = readFileSync(PLIST, "utf8");
102
+ } catch {
103
+ return null;
104
+ }
105
+ const value = (name) =>
106
+ plist.match(
107
+ new RegExp(`<string>--${name}</string>\\s*<string>([^<]*)</string>`),
108
+ )?.[1];
109
+ const home = value("home");
110
+ if (!home) return null;
111
+ return {
112
+ home: unxml(home),
113
+ port: Number(value("port")) || null,
114
+ // Written by `autostart` before there was a copy of its own to run: from a global install,
115
+ // and without the mark that tells Settings it is the background
116
+ legacy: !plist.includes("<key>THURSDAY_BACKGROUND</key>"),
117
+ };
118
+ }
119
+
120
+ /**
121
+ * The job as launchd has it: the pid of the running starter, and how it last exited. Null when
122
+ * launchd has no such job loaded.
123
+ */
124
+ function jobState() {
125
+ const printed = launchctl("print", `${domain()}/${LABEL}`);
126
+ if (printed.status !== 0) return null;
127
+ return {
128
+ pid: Number(printed.stdout.match(/^\s*pid = (\d+)/m)?.[1]) || null,
129
+ exit:
130
+ printed.stdout.match(/^\s*last exit code = (.+)$/m)?.[1]?.trim() ?? null,
131
+ };
132
+ }
133
+
134
+ /** A word the shell takes as it is, whatever it holds. */
135
+ const shellQuoted = (text) => `'${text.replace(/'/g, `'\\''`)}'`;
136
+
137
+ /**
138
+ * What launchd runs: a shell that finds a Node and starts the copy on it. The Node that ran
139
+ * `start` comes first, then `node` on the PATH it was given, then where Node's installers and
140
+ * version managers keep one that stays put. The first alone broke on the day it went: a version
141
+ * manager keeps each version in a folder of its own and removes it with the version (`nvm
142
+ * uninstall`), and Homebrew removes the file behind its link on an upgrade — the job then failed
143
+ * at every login. Each is tried with the copy's own version check (node-check.mjs), so one gone,
144
+ * broken or too old is passed over, and the log says which ran when it was not the first; with
145
+ * none, the log says so and launchd tries again in half a minute, which picks up a Node
146
+ * installed meanwhile. The log is kept to its size here too: a job that fails at once is started
147
+ * every half minute, and nothing else of it runs to do it.
148
+ */
149
+ function launcherOf({ cli, log }) {
150
+ const home = homedir();
151
+ const env = process.env;
152
+ const stays = [
153
+ // Homebrew on Apple silicon; on Intel, and nodejs.org's installer, and n
154
+ "/opt/homebrew/bin/node",
155
+ "/usr/local/bin/node",
156
+ // Version managers' own launchers, which run their default version
157
+ join(env.VOLTA_HOME || join(home, ".volta"), "bin", "node"),
158
+ join(env.ASDF_DATA_DIR || join(home, ".asdf"), "shims", "node"),
159
+ join(
160
+ env.MISE_DATA_DIR || join(home, ".local", "share", "mise"),
161
+ "shims",
162
+ "node",
163
+ ),
164
+ ...[
165
+ env.FNM_DIR,
166
+ join(home, ".local", "share", "fnm"),
167
+ join(home, "Library", "Application Support", "fnm"),
168
+ ]
169
+ .filter(Boolean)
170
+ .map((dir) => join(dir, "aliases", "default", "bin", "node")),
171
+ ].map(shellQuoted);
172
+ // nvm has no launcher of its own: every version it holds, left to the shell to list
173
+ const nvm = `${shellQuoted(join(env.NVM_DIR || join(home, ".nvm"), "versions", "node"))}/*/bin/node`;
174
+ const stamp = `$(date '+%Y-%m-%d %H:%M:%S')`;
175
+ return `log=${shellQuoted(log)}
176
+ if [ -f "$log" ] && [ "$(stat -f %z "$log")" -gt ${LOG_BYTES} ]; then cp "$log" "$log.1" && : > "$log"; fi
177
+ first=${shellQuoted(process.execPath)}
178
+ check=${shellQuoted(join(dirname(cli), "node-check.mjs"))}
179
+ seen=
180
+ for node in "$first" "$(command -v node)" ${stays.join(" ")} ${nvm}; do
181
+ [ -x "$node" ] || continue
182
+ if "$node" "$check" > /dev/null 2>&1; then
183
+ [ "$node" = "$first" ] || echo "${stamp} $first did not run: starting on $node" >&2
184
+ exec "$node" "$@"
185
+ fi
186
+ seen=$node
187
+ done
188
+ echo "${stamp} No Node to start Thursday on: $first did not run, and no other that does was found." >&2
189
+ [ -z "$seen" ] || "$seen" "$check" >&2
190
+ echo "Once one is installed, it starts on it within half a minute." >&2
191
+ exit 127
192
+ `;
193
+ }
194
+
195
+ const plistOf = ({
196
+ cli,
197
+ home,
198
+ port,
199
+ log,
200
+ }) => `<?xml version="1.0" encoding="UTF-8"?>
201
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
202
+ <plist version="1.0">
203
+ <dict>
204
+ <key>Label</key>
205
+ <string>${LABEL}</string>
206
+ <!-- The home and the port are written in rather than left to be found: launchd starts a
207
+ job in no folder of its own, and a server that quietly moved to another port would
208
+ leave the installed window looking at an address nothing answers on. -->
209
+ <key>ProgramArguments</key>
210
+ <array>
211
+ <!-- A shell that finds a Node to run it on: the one that ran "start" while it is there,
212
+ and another when a version manager or an upgrade has removed it -->
213
+ <string>/bin/sh</string>
214
+ <string>-c</string>
215
+ <string>${xml(launcherOf({ cli, log }))}</string>
216
+ <string>${LABEL}</string>
217
+ <string>${xml(cli)}</string>
218
+ <string>--home</string>
219
+ <string>${xml(home)}</string>
220
+ <string>--port</string>
221
+ <string>${port}</string>
222
+ <string>--no-open</string>
223
+ </array>
224
+ <!-- launchd hands a job a PATH with none of what a person installs, and every shell a bot
225
+ runs inherits the server's (features/workspace). This is the PATH of the terminal that
226
+ started it. -->
227
+ <key>EnvironmentVariables</key>
228
+ <dict>
229
+ <key>PATH</key>
230
+ <string>${xml(personPath())}</string>
231
+ <!-- How the starter knows it is this job, and tells Settings where it runs -->
232
+ <key>THURSDAY_BACKGROUND</key>
233
+ <string>1</string>
234
+ <!-- What the person typed to start it, for every line that tells them what to type next:
235
+ the copy that runs here cannot tell npx from a global install -->
236
+ <key>THURSDAY_COMMAND</key>
237
+ <string>${xml(thursdayCommand())}</string>
238
+ </dict>
239
+ <key>RunAtLoad</key>
240
+ <true/>
241
+ <!-- Started again whenever it stops, so a crash in the night is up by morning. A server
242
+ that cannot open its database exits the same way; the wait keeps that to twice a
243
+ minute, and the log says why each time. -->
244
+ <key>KeepAlive</key>
245
+ <true/>
246
+ <key>ThrottleInterval</key>
247
+ <integer>30</integer>
248
+ <key>StandardOutPath</key>
249
+ <string>${xml(log)}</string>
250
+ <key>StandardErrorPath</key>
251
+ <string>${xml(log)}</string>
252
+ </dict>
253
+ </plist>
254
+ `;
255
+
256
+ /**
257
+ * The PATH of the terminal that ran this, without what npm put ahead of it. Run through npx, the
258
+ * PATH also holds npx's cache and the `node_modules/.bin` of the folder it was run from and of
259
+ * every folder above it, and a login item started inside a project found that project's tools
260
+ * first — `node` included — from then on. npm puts those first, then its own node-gyp-bin,
261
+ * then the PATH it was given (@npmcli/run-script set-path.js).
262
+ */
263
+ function personPath() {
264
+ const dirs = (process.env.PATH ?? "").split(delimiter);
265
+ const npm = dirs.findLastIndex((dir) => dir.endsWith(`${sep}node-gyp-bin`));
266
+ return dirs.slice(npm + 1).join(delimiter);
267
+ }
268
+
269
+ /**
270
+ * The job's file, written beside where it goes: moved into place, it is there whole or not at
271
+ * all, and a full disk leaves the one before rather than half of a new one. Never writable by
272
+ * the group, which launchd refuses to load. The staged path, for the caller to move.
273
+ */
274
+ function stagePlist(text) {
275
+ mkdirSync(AGENTS, { recursive: true });
276
+ const staged = `${PLIST}.new`;
277
+ rmSync(staged, { force: true });
278
+ writeFileSync(staged, text, { mode: 0o644 });
279
+ return staged;
280
+ }
281
+
282
+ /** Waits without giving up the thread, for a Ctrl+C that has to finish before it exits. */
283
+ const pause = (ms) =>
284
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
285
+
286
+ /**
287
+ * Takes the job out and waits for launchd to let go of it: it keeps the job's record until the
288
+ * server has parked its jobs and exited, and a job loaded under the same name meanwhile is
289
+ * refused as one already there. Blocking, for a Ctrl+C that has to finish before it exits.
290
+ */
291
+ function bootOut() {
292
+ const home = backgroundJob()?.home;
293
+ const out = launchctl("bootout", `${domain()}/${LABEL}`);
294
+ for (let waited = 0; waited < FREE_MS && jobState(); waited += 250)
295
+ pause(250);
296
+ if (home) endLeftover(home);
297
+ return out;
298
+ }
299
+
300
+ /**
301
+ * Ends a server of the job's that outlived it. launchd signals the process it started, and a
302
+ * launcher that runs node as its child — a version manager's — need not pass the signal on: its
303
+ * server went on holding the folder and the port with no job left to stop it. Only one marked
304
+ * as the background's (lock.mjs); asked to stop first, as launchd would have, and ended with
305
+ * what it started when it has not within the wait.
306
+ */
307
+ function endLeftover(home) {
308
+ const left = runningOn(home);
309
+ if (!left?.background) return;
310
+ const gone = () => runningOn(home)?.pid !== left.pid;
311
+ try {
312
+ process.kill(left.pid, "SIGTERM");
313
+ } catch {
314
+ return;
315
+ }
316
+ for (let waited = 0; waited < FREE_MS && !gone(); waited += 250) pause(250);
317
+ if (gone()) return;
318
+ spawnSync("pkill", ["-KILL", "-P", String(left.pid)]);
319
+ try {
320
+ process.kill(left.pid, "SIGKILL");
321
+ } catch {
322
+ // Gone meanwhile
323
+ }
324
+ }
325
+
326
+ /** Whether the port is free, giving a server that was just stopped time to let go of it. */
327
+ async function waitFree(port) {
328
+ for (let waited = 0; waited < FREE_MS; waited += 250) {
329
+ if (!(await portTaken(port))) return true;
330
+ await sleep(250);
331
+ }
332
+ return !(await portTaken(port));
333
+ }
334
+
335
+ const same = (a, b) => {
336
+ try {
337
+ return realpathSync(a) === realpathSync(b);
338
+ } catch {
339
+ return false;
340
+ }
341
+ };
342
+
343
+ /**
344
+ * Whether a copy installed under PROGRAM is whole, of this version and for this machine. npm
345
+ * writes a package's files in no order that ends on one to look for: a copy cut short — killed,
346
+ * the power gone — held its manifest and its server and not the rest, and a data folder carried
347
+ * to a Mac of the other kind carries native code that does not run there. The mark is written
348
+ * once npm has finished, for the machine it ran on.
349
+ */
350
+ function intact(folder, version) {
351
+ try {
352
+ const mark = JSON.parse(readFileSync(join(folder, INSTALLED), "utf8"));
353
+ return (
354
+ mark.version === version &&
355
+ mark.platform === process.platform &&
356
+ mark.arch === process.arch
357
+ );
358
+ } catch {
359
+ return false;
360
+ }
361
+ }
362
+
363
+ /**
364
+ * The copy the job runs. A checkout runs itself: its build is the point of it. Any other copy
365
+ * is installed under PROGRAM, one folder per version, with npm and from the very files that are
366
+ * running — `--install-links` packs a folder rather than linking it — so the job runs what was
367
+ * just run, from npm's cache and without the network. npx's own copy lives in a cache npm
368
+ * empties, and a login item that pointed there broke the day it did. A version of its own
369
+ * leaves the copy that runs now untouched until the new one is up.
370
+ */
371
+ function installCopy(root, version) {
372
+ if (isCheckout(root)) return root;
373
+ const folder = join(PROGRAM, version);
374
+ const installed = join(folder, "node_modules", "thursday-agent");
375
+ if (same(root, installed) || intact(folder, version)) return installed;
376
+ // Half a copy from an install that was cut short, or one for another machine
377
+ rmSync(folder, { recursive: true, force: true });
378
+ mkdirSync(folder, { recursive: true });
379
+ writeFileSync(
380
+ join(folder, "package.json"),
381
+ `${JSON.stringify({ private: true })}\n`,
382
+ );
383
+ const npm = spawnSync(
384
+ "npm",
385
+ [
386
+ "install",
387
+ "--prefix",
388
+ folder,
389
+ "--install-links",
390
+ "--prefer-offline",
391
+ "--no-audit",
392
+ "--no-fund",
393
+ "--loglevel=error",
394
+ root,
395
+ ],
396
+ { encoding: "utf8", stdio: ["ignore", "ignore", "pipe"] },
397
+ );
398
+ if (npm.error?.code === "ENOENT")
399
+ throw new Error("npm was not found. It comes with Node.");
400
+ if (npm.error || npm.status !== 0)
401
+ throw new Error(
402
+ npm.stderr?.trim() || npm.error?.message || `npm exited ${npm.status}`,
403
+ );
404
+ let manifest = null;
405
+ try {
406
+ manifest = JSON.parse(
407
+ readFileSync(join(installed, "package.json"), "utf8"),
408
+ );
409
+ } catch {
410
+ // Said below
411
+ }
412
+ if (
413
+ manifest?.version !== version ||
414
+ !existsSync(join(installed, "server.js"))
415
+ )
416
+ throw new Error(`npm finished, but ${installed} is not a whole copy.`);
417
+ writeFileSync(
418
+ join(folder, INSTALLED),
419
+ `${JSON.stringify({ version, platform: process.platform, arch: process.arch })}\n`,
420
+ );
421
+ return installed;
422
+ }
423
+
424
+ /** Every version installed under PROGRAM but the one the job now runs. */
425
+ function pruneCopies(program) {
426
+ let names;
427
+ try {
428
+ names = readdirSync(PROGRAM);
429
+ } catch {
430
+ return;
431
+ }
432
+ for (const name of names) {
433
+ const folder = join(PROGRAM, name);
434
+ if (/^\d+\.\d+\.\d+/.test(name) && !program.startsWith(`${folder}${sep}`))
435
+ rmSync(folder, { recursive: true, force: true });
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Writes back and loads the job file there was before a start that did not work; with none
441
+ * before, no job is left behind to fail again at every login. Whether one was loaded.
442
+ */
443
+ function reload(before) {
444
+ if (before !== null) {
445
+ try {
446
+ renameSync(stagePlist(before), PLIST);
447
+ if (launchctl("bootstrap", domain(), PLIST).status === 0) return true;
448
+ } catch {
449
+ // Said by the caller: nothing runs in the background
450
+ }
451
+ }
452
+ rmSync(PLIST, { force: true });
453
+ return false;
454
+ }
455
+
456
+ /**
457
+ * Puts back the job there was, and says whether it serves again. One that no longer can — its
458
+ * database already moved on by the version that failed, its copy gone — is taken out too: left
459
+ * in, launchd would start it every half minute and at every login. The line to print.
460
+ */
461
+ async function putBack(before, job) {
462
+ if (!reload(before))
463
+ return before === null
464
+ ? "Nothing runs in the background now."
465
+ : "The one that ran before could not be started again, so nothing runs in the background now.";
466
+ if (!job?.port) return "The one that ran before is loaded again.";
467
+ if (await waitUp(job.port, job.home))
468
+ return `The one that ran before runs again: http://localhost:${job.port}`;
469
+ bootOut();
470
+ rmSync(PLIST, { force: true });
471
+ return "The one that ran before did not come back either, so nothing runs in the background now.";
472
+ }
473
+
474
+ /** How far the log runs now, so a failure shows only what this start wrote after it. */
475
+ function logSize(log) {
476
+ try {
477
+ return statSync(log).size;
478
+ } catch {
479
+ return 0;
480
+ }
481
+ }
482
+
483
+ /**
484
+ * The last lines the log gained since `from`, for a start that did not come up. Read from its
485
+ * end: it grows for as long as the job runs. One started over since (LOG_BYTES) is read from
486
+ * its start.
487
+ */
488
+ function logTail(log, from, lines = 12) {
489
+ let fd;
490
+ try {
491
+ fd = openSync(log, "r");
492
+ const size = fstatSync(fd).size;
493
+ const start = Math.max(size < from ? 0 : from, size - 16_384);
494
+ const text = Buffer.alloc(Math.max(0, size - start));
495
+ readSync(fd, text, 0, text.length, start);
496
+ return text.toString("utf8").trimEnd().split("\n").slice(-lines);
497
+ } catch {
498
+ return [];
499
+ } finally {
500
+ if (fd !== undefined) closeSync(fd);
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Keeps the log to its size while the server runs in the background, where it runs for weeks:
506
+ * past LOG_BYTES it is copied aside as `.1`, over the one before, and started over. launchd
507
+ * holds it open for appending, so what the server writes next lands at the new start.
508
+ */
509
+ export function keepLogShort(home) {
510
+ const log = join(home, LOG_FILE);
511
+ const check = () => {
512
+ try {
513
+ if (statSync(log).size <= LOG_BYTES) return;
514
+ copyFileSync(log, `${log}.1`);
515
+ truncateSync(log, 0);
516
+ } catch {
517
+ // A log that cannot be kept short is not a reason to stop serving
518
+ }
519
+ };
520
+ check();
521
+ setInterval(check, LOG_CHECK_MS).unref();
522
+ }
523
+
524
+ /**
525
+ * Whether the server answers a page. The port alone is not enough: Next listens first and
526
+ * migrates the database after, and a server that cannot migrate held the port for a moment
527
+ * before it exited — taken for a start that worked.
528
+ */
529
+ async function answers(port) {
530
+ try {
531
+ const response = await fetch(`http://127.0.0.1:${port}/`, {
532
+ redirect: "manual",
533
+ signal: AbortSignal.timeout(15_000),
534
+ });
535
+ return response.status < 500;
536
+ } catch {
537
+ return false;
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Waits for the job to serve a page on this folder. The page alone is not enough: a server a
543
+ * terminal started meanwhile answers on the same port, and was taken for the job while the job
544
+ * failed. A job that stopped with a failure meanwhile is not waited on — launchd would start
545
+ * it again in half a minute, and it would fail the same way.
546
+ */
547
+ async function waitUp(port, home) {
548
+ const until = Date.now() + UP_MS;
549
+ const since = Date.now();
550
+ while (Date.now() < until) {
551
+ const state = jobState();
552
+ if (!state) return false;
553
+ if (
554
+ state.pid &&
555
+ heldBy(runningOn(home), state.pid) &&
556
+ (await answers(port))
557
+ )
558
+ return true;
559
+ if (!state.pid && failed(state.exit) && Date.now() - since > 2000)
560
+ return false;
561
+ await sleep(500);
562
+ }
563
+ return false;
564
+ }
565
+
566
+ /** An exit launchd recorded that was a failure; "(never exited)" is a job not yet run. */
567
+ const failed = (exit) => {
568
+ const code = Number.parseInt(exit ?? "", 10);
569
+ return Number.isInteger(code) && code !== 0;
570
+ };
571
+
572
+ /** Remembers that the person decided, so a run in a terminal does not ask again. */
573
+ function markDecided(home) {
574
+ try {
575
+ mkdirSync(home, { recursive: true });
576
+ writeFileSync(join(home, DECIDED_FILE), "no\n");
577
+ } catch {
578
+ // A folder that cannot be written only costs the question once more
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Takes the start lock. The process of the start that holds it, when another does; a lock left
584
+ * by a start that was killed names a process that is gone, and is taken over. A temporary folder
585
+ * that cannot be written costs only this guard.
586
+ */
587
+ function holdStart() {
588
+ const mine = JSON.stringify({
589
+ pid: process.pid,
590
+ startedUtc: startedAt(process.pid, "UTC") || undefined,
591
+ });
592
+ for (let tries = 0; tries < 2; tries++) {
593
+ try {
594
+ writeFileSync(START_LOCK, mine, { flag: "wx" });
595
+ return null;
596
+ } catch (error) {
597
+ if (error?.code !== "EEXIST") return null;
598
+ }
599
+ let held = null;
600
+ try {
601
+ held = JSON.parse(readFileSync(START_LOCK, "utf8"));
602
+ } catch {
603
+ // Unreadable: nobody's
604
+ }
605
+ if (Number.isInteger(held?.pid) && stillRuns(held)) return held.pid;
606
+ rmSync(START_LOCK, { force: true });
607
+ }
608
+ return null;
609
+ }
610
+
611
+ const letGoOfStart = () => {
612
+ try {
613
+ if (JSON.parse(readFileSync(START_LOCK, "utf8")).pid === process.pid)
614
+ rmSync(START_LOCK, { force: true });
615
+ } catch {
616
+ // Gone already
617
+ }
618
+ };
619
+
620
+ /** Lines to print, indented; null is a line left out, "" an empty one. */
621
+ const block = (lines) =>
622
+ `\n${lines
623
+ .filter((line) => line !== null)
624
+ .map((line) => (line ? ` ${line}` : ""))
625
+ .join("\n")}\n`;
626
+ const say = (lines) => console.log(block(lines));
627
+ const fail = (lines) => console.error(block(lines));
628
+
629
+ /**
630
+ * Starts the server in the background and has it start when the person logs in; a job already
631
+ * there — another version, another data folder, another port, or one that stopped — is
632
+ * replaced, and put back if the new one does not come up. True once it serves; "held" when
633
+ * another server or another start holds this folder, so nothing is to be served here either;
634
+ * false otherwise. Everything it could not do is said before it returns.
635
+ */
636
+ export async function startInBackground({
637
+ root,
638
+ home,
639
+ port: asked,
640
+ open,
641
+ version,
642
+ }) {
643
+ if (!mac()) {
644
+ fail([
645
+ "Running in the background is macOS only for now.",
646
+ `Elsewhere, run ${commandFor("", home)} in a terminal and leave it open,`,
647
+ "or add it to the programs that start when you log in.",
648
+ ]);
649
+ return false;
650
+ }
651
+ const another = holdStart();
652
+ if (another) {
653
+ fail([
654
+ `Another start is setting it up already (process ${another}).`,
655
+ "Run this again once it is done.",
656
+ ]);
657
+ return "held";
658
+ }
659
+ try {
660
+ return await replaceJob({ root, home, asked, open, version });
661
+ } finally {
662
+ letGoOfStart();
663
+ }
664
+ }
665
+
666
+ async function replaceJob({ root, home, asked, open, version }) {
667
+ const command = thursdayCommand();
668
+ const state = jobState();
669
+ const job = backgroundJob();
670
+ const running = runningOn(home);
671
+ // A server on this folder that is not the job: two must never share one database, and
672
+ // stopping a person's terminal for them is not this command's to do
673
+ if (running && !heldBy(running, state?.pid)) {
674
+ fail([
675
+ `Thursday is already running on this data folder${running.url ? `: ${running.url}` : ""}`,
676
+ ...stopLines(running.pid, home),
677
+ "Then run this again.",
678
+ ]);
679
+ return "held";
680
+ }
681
+ // Nothing to change: this version serves this folder already, on the port asked for, from a
682
+ // job this version wrote. One an earlier `autostart` wrote is written again
683
+ if (
684
+ running &&
685
+ job?.home === home &&
686
+ !job.legacy &&
687
+ running.version === version &&
688
+ (asked === undefined || Number(asked) === job.port)
689
+ ) {
690
+ say([
691
+ `Thursday ${version} already runs in the background: ${running.url}`,
692
+ open ? "Opened it in your browser." : null,
693
+ ]);
694
+ if (open && running.url) openBrowser(running.url);
695
+ return true;
696
+ }
697
+
698
+ const port = Number(asked ?? homePort(home));
699
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
700
+ fail([`Not a port: ${asked}`]);
701
+ return false;
702
+ }
703
+ // The job about to be replaced may hold it; anything else holding it is not about to move
704
+ const busy = [
705
+ `Something else serves on port ${port}.`,
706
+ "Stop it, or choose another with --port, and run this again.",
707
+ ];
708
+ if (!(state?.pid && job?.port === port) && (await portTaken(port))) {
709
+ fail(busy);
710
+ return false;
711
+ }
712
+
713
+ console.log("\n Setting up the copy that runs in the background…");
714
+ let program;
715
+ try {
716
+ program = installCopy(root, version);
717
+ } catch (error) {
718
+ fail([
719
+ `Could not install it into ${PROGRAM}:`,
720
+ ...String(error?.message ?? error)
721
+ .split("\n")
722
+ .slice(-6),
723
+ "",
724
+ "Nothing was changed. Your data is as it was.",
725
+ ]);
726
+ return false;
727
+ }
728
+
729
+ // Written before the job there is stopped: a folder that cannot be written, or a full disk,
730
+ // then leaves what runs running
731
+ const log = join(home, LOG_FILE);
732
+ let staged;
733
+ try {
734
+ mkdirSync(home, { recursive: true });
735
+ staged = stagePlist(
736
+ plistOf({ cli: join(program, "bin", "thursday.mjs"), home, port, log }),
737
+ );
738
+ } catch (error) {
739
+ fail([
740
+ `Could not write ${PLIST}:`,
741
+ String(error?.message ?? error),
742
+ "",
743
+ "Nothing was changed. Your data is as it was.",
744
+ ]);
745
+ return false;
746
+ }
747
+
748
+ // Not loaded is not a failure: this is also how an earlier job lets go before a new one
749
+ const before = existsSync(PLIST) ? readFileSync(PLIST, "utf8") : null;
750
+ // From here until the new one serves, the job there was is out: Ctrl+C, or the terminal
751
+ // closed, loads it again rather than leave nothing, or a new job that may not work, loaded.
752
+ // Heard once: npx passes the terminal's Ctrl+C on as a second one, which ended this halfway
753
+ let interrupting = false;
754
+ const interrupted = () => {
755
+ if (interrupting) return;
756
+ interrupting = true;
757
+ bootOut();
758
+ rmSync(staged, { force: true });
759
+ say([
760
+ "Stopped.",
761
+ reload(before)
762
+ ? "The one that ran before is loaded again."
763
+ : "Nothing runs in the background now.",
764
+ ]);
765
+ // Exiting skips the `finally` that would
766
+ letGoOfStart();
767
+ process.exit(130);
768
+ };
769
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
770
+ for (const signal of signals) process.on(signal, interrupted);
771
+ try {
772
+ if (state) {
773
+ bootOut();
774
+ if (job?.port) await waitFree(job.port);
775
+ }
776
+ if (!(await waitFree(port))) {
777
+ rmSync(staged, { force: true });
778
+ fail([...busy, await putBack(before, job)]);
779
+ return false;
780
+ }
781
+
782
+ const from = logSize(log);
783
+ try {
784
+ renameSync(staged, PLIST);
785
+ } catch (error) {
786
+ fail([
787
+ `Could not write ${PLIST}:`,
788
+ String(error?.message ?? error),
789
+ await putBack(before, job),
790
+ ]);
791
+ return false;
792
+ }
793
+ // Asked for by name: a turn-off left from before (`launchctl disable`) is undone, which
794
+ // launchctl otherwise refuses as "Input/output error"
795
+ launchctl("enable", `${domain()}/${LABEL}`);
796
+ const started = launchctl("bootstrap", domain(), PLIST);
797
+ if (started.status !== 0) {
798
+ fail([
799
+ "macOS refused to start it in the background:",
800
+ started.stderr?.trim() || `launchctl exited ${started.status}`,
801
+ "If it is turned off under System Settings › General › Login Items, turn it on there.",
802
+ await putBack(before, job),
803
+ ]);
804
+ return false;
805
+ }
806
+
807
+ if (!(await waitUp(port, home))) {
808
+ // Taken back out: left in, launchd would start it every half minute and at every login,
809
+ // failing the same way each time
810
+ bootOut();
811
+ // Read before the one that ran before writes to the same log again
812
+ const why = logTail(log, from);
813
+ const back = await putBack(before, job);
814
+ fail([
815
+ "It did not come up in the background. The log says why:",
816
+ "",
817
+ ...why,
818
+ "",
819
+ `Log: ${log}`,
820
+ back,
821
+ `Run in a terminal, ${commandFor("", home)}, and it can ask what to do about it.`,
822
+ ]);
823
+ return false;
824
+ }
825
+ } finally {
826
+ for (const signal of signals) process.off(signal, interrupted);
827
+ }
828
+ pruneCopies(program);
829
+
830
+ const url = `http://localhost:${port}`;
831
+ say([
832
+ `Thursday ${version} runs in the background, and starts when you log in.`,
833
+ url,
834
+ `data: ${home}`,
835
+ // One job serves one folder: the one it served before is left, and said so
836
+ job && job.home !== home
837
+ ? `It no longer serves ${job.home}, which is kept as it is.`
838
+ : null,
839
+ open ? "Opened it in your browser. This terminal can be closed." : null,
840
+ `To stop it: ${command} stop`,
841
+ ]);
842
+ if (open) openBrowser(url);
843
+ return true;
844
+ }
845
+
846
+ /**
847
+ * Stops the background job, and it no longer starts at login. The data folder is untouched.
848
+ * False when macOS kept it running, which is said.
849
+ */
850
+ export async function stopBackground({ home }) {
851
+ const job = backgroundJob();
852
+ const state = mac() ? jobState() : null;
853
+ const terminal = runningOn(home);
854
+
855
+ if (!job && !state) {
856
+ say([
857
+ "Thursday was not running in the background.",
858
+ ...(terminal ? stopLines(terminal.pid, home) : []),
859
+ ]);
860
+ return true;
861
+ }
862
+
863
+ if (state) {
864
+ // A server slow to park its jobs keeps its record past the wait: a refusal is launchctl
865
+ // saying it failed, with the job still loaded
866
+ const out = bootOut();
867
+ if (out.status !== 0 && jobState()) {
868
+ fail([
869
+ "macOS did not stop it:",
870
+ out.stderr?.trim() || `launchctl exited ${out.status}`,
871
+ ]);
872
+ return false;
873
+ }
874
+ } else if (job) endLeftover(job.home);
875
+ rmSync(PLIST, { force: true });
876
+ // A server parks its running jobs before it exits, which can outlast the wait
877
+ const freed = !job?.port || (await waitFree(job.port));
878
+ // Stopped on purpose: a later run in a terminal does not ask again
879
+ markDecided(job?.home ?? home);
880
+ say([
881
+ "Thursday stopped, and no longer starts when you log in.",
882
+ freed
883
+ ? null
884
+ : `Port ${job.port} is still in use; it may take a moment more.`,
885
+ job ? `data: ${job.home} (kept as it is)` : null,
886
+ `Start it again: ${commandFor("start", job?.home ?? home)}`,
887
+ ]);
888
+ return true;
889
+ }
890
+
891
+ /** Where Thursday runs, if it does, and how to reach and stop it. */
892
+ export function printStatus({ home }) {
893
+ const command = thursdayCommand();
894
+ const job = backgroundJob();
895
+ const state = mac() && job ? jobState() : null;
896
+ const served = job ? runningOn(job.home) : null;
897
+
898
+ if (job && heldBy(served, state?.pid)) {
899
+ say([
900
+ `Thursday${served.version ? ` ${served.version}` : ""} runs in the background, and starts when you log in.`,
901
+ served.url,
902
+ `data: ${job.home}`,
903
+ `log: ${join(job.home, LOG_FILE)}`,
904
+ `To stop it: ${command} stop`,
905
+ ]);
906
+ return;
907
+ }
908
+ if (job) {
909
+ const log = join(job.home, LOG_FILE);
910
+ const off = new RegExp(`"${LABEL}" => (disabled|true)`).test(
911
+ launchctl("print-disabled", domain()).stdout ?? "",
912
+ );
913
+ say([
914
+ "Thursday is set to start when you log in, but is not running now.",
915
+ off
916
+ ? "macOS has it turned off (launchctl disable); starting it again turns it on."
917
+ : null,
918
+ failed(state?.exit)
919
+ ? `It last stopped with exit ${state.exit}. The end of its log:`
920
+ : "The end of its log:",
921
+ "",
922
+ ...logTail(log, 0, 6),
923
+ "",
924
+ `Log: ${log}`,
925
+ `Try again: ${commandFor("start", job.home)} · Stop trying: ${command} stop`,
926
+ ]);
927
+ return;
928
+ }
929
+ const terminal = runningOn(home);
930
+ if (terminal) {
931
+ say([
932
+ `Thursday runs on this data folder${terminal.url ? `: ${terminal.url}` : ""}, not in the background.`,
933
+ ...stopLines(terminal.pid, home),
934
+ mac()
935
+ ? `To run it in the background instead, stop it and run: ${commandFor("start", home)}`
936
+ : null,
937
+ ]);
938
+ return;
939
+ }
940
+ say([
941
+ "Thursday is not running.",
942
+ mac() ? `In the background: ${commandFor("start", home)}` : null,
943
+ `In this terminal: ${commandFor("", home)}`,
944
+ ]);
945
+ }
946
+
947
+ /**
948
+ * The one question a first run asks: whether to keep Thursday running in the background.
949
+ * Asked of a person at a terminal on a Mac, with no job yet and nothing decided before; a
950
+ * checkout is someone working on the app, who runs it by hand. "background" once it runs
951
+ * there, and this run has nothing left to do; "terminal" to serve here; "refused" when another
952
+ * server or start took this folder while the question waited, and serving here would make two.
953
+ */
954
+ export async function offerBackground({ root, home, port, open, version }) {
955
+ if (!mac() || !process.stdin.isTTY || !process.stdout.isTTY)
956
+ return "terminal";
957
+ if (process.env.CI || isCheckout(root)) return "terminal";
958
+ if (existsSync(PLIST) || existsSync(join(home, DECIDED_FILE)))
959
+ return "terminal";
960
+
961
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
962
+ // Raw mode: Ctrl+C arrives here as a key, not as a signal
963
+ rl.on("SIGINT", () => {
964
+ console.log();
965
+ process.exit(130);
966
+ });
967
+ // Ctrl+D ends the input and rejects the question: an answer nobody gave is not a yes, and
968
+ // not a decision to remember either — the next run asks again
969
+ const answer = await rl
970
+ .question(
971
+ "\n Keep Thursday running in the background, and start it when you log in? [Y/n] ",
972
+ )
973
+ .catch((error) => {
974
+ if (error?.name === "AbortError") return null;
975
+ throw error;
976
+ });
977
+ rl.close();
978
+
979
+ if (answer === null) {
980
+ console.log();
981
+ return "terminal";
982
+ }
983
+ if (/^n/i.test(answer.trim())) {
984
+ markDecided(home);
985
+ console.log(
986
+ `\n It runs in this terminal until you close it.\n To keep it running in the background later: ${commandFor("start", home)}`,
987
+ );
988
+ return "terminal";
989
+ }
990
+ const started = await startInBackground({ root, home, port, open, version });
991
+ if (started === true) return "background";
992
+ if (started === "held") return "refused";
993
+ console.log(" Running it in this terminal instead.");
994
+ return "terminal";
995
+ }