@starlove/orbit 1.4.0 → 2.0.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 (329) hide show
  1. package/Dockerfile +10 -8
  2. package/README.md +146 -164
  3. package/dist/full/csp.txt +1 -0
  4. package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-subset.woff2 +0 -0
  5. package/dist/full/fonts/JetBrains_Mono/README.txt +5 -0
  6. package/dist/full/fonts/Mona_Sans/MonaSans-subset.woff2 +0 -0
  7. package/{public/fonts/Open_Sans → dist/full/fonts/Mona_Sans}/OFL.txt +93 -93
  8. package/dist/full/fonts/Mona_Sans/README.txt +10 -0
  9. package/{public/fonts/JetBrains_Mono → dist/full/fonts/Newsreader}/OFL.txt +93 -93
  10. package/dist/full/fonts/Newsreader/README.txt +8 -0
  11. package/dist/full/fonts/Newsreader/newsreader.woff2 +0 -0
  12. package/dist/full/index.html +36 -653
  13. package/dist/full/js/app.BmuponPJ.js +32 -0
  14. package/dist/full/js/app.BmuponPJ.js.map +1 -0
  15. package/dist/full/js/chunk.1fJGQGbW.js +2 -0
  16. package/dist/full/js/chunk.1fJGQGbW.js.map +1 -0
  17. package/dist/full/js/chunk.5D7JO2zp.js +2 -0
  18. package/dist/full/js/chunk.5D7JO2zp.js.map +1 -0
  19. package/dist/full/js/chunk.BrNn_D79.js +2 -0
  20. package/dist/full/js/chunk.BrNn_D79.js.map +1 -0
  21. package/dist/full/js/chunk.CokJXIqM.js +2 -0
  22. package/dist/full/js/chunk.CokJXIqM.js.map +1 -0
  23. package/dist/full/js/chunk.D_Haqy6R.js +2 -0
  24. package/dist/full/js/chunk.D_Haqy6R.js.map +1 -0
  25. package/dist/full/js/chunk.RO0jsH6I.js +15 -0
  26. package/dist/full/js/chunk.RO0jsH6I.js.map +1 -0
  27. package/dist/full/js/chunk.dqWOEtBM.js +2 -0
  28. package/dist/full/js/chunk.dqWOEtBM.js.map +1 -0
  29. package/dist/full/js/warm.f260bbbc.js +15 -0
  30. package/dist/full/orbits-favicon.png +0 -0
  31. package/dist/full/orbits-logo-bg-wide.png +0 -0
  32. package/dist/full/orbits-logo-bg.png +0 -0
  33. package/dist/full/orbits-logo.png +0 -0
  34. package/dist/full/styles.2b154ffe.css +1 -0
  35. package/dist/full/vendor/starlove-ui/theme-init.js +66 -0
  36. package/dist/full/version.json +2 -2
  37. package/docs/AGENT_GUIDE.md +114 -0
  38. package/docs/UPDATING.md +249 -64
  39. package/lib/adapters/indexeddb/blobs.d.ts +3 -0
  40. package/lib/adapters/indexeddb/blobs.js +54 -0
  41. package/lib/adapters/indexeddb/database.d.ts +60 -0
  42. package/lib/adapters/indexeddb/database.js +126 -0
  43. package/lib/adapters/indexeddb/index.d.ts +5 -0
  44. package/lib/adapters/indexeddb/index.js +3 -0
  45. package/lib/adapters/indexeddb/locks.d.ts +14 -0
  46. package/lib/adapters/indexeddb/locks.js +40 -0
  47. package/lib/adapters/indexeddb/store.d.ts +40 -0
  48. package/lib/adapters/indexeddb/store.js +424 -0
  49. package/lib/adapters/indexeddb/tables.d.ts +43 -0
  50. package/lib/adapters/indexeddb/tables.js +127 -0
  51. package/lib/adapters/memory/blobs.d.ts +3 -0
  52. package/lib/adapters/memory/blobs.js +44 -0
  53. package/lib/adapters/memory/board.d.ts +41 -0
  54. package/lib/adapters/memory/board.js +521 -0
  55. package/lib/adapters/memory/index.d.ts +2 -0
  56. package/lib/adapters/memory/index.js +2 -0
  57. package/lib/adapters/memory/search.d.ts +6 -0
  58. package/lib/adapters/memory/search.js +30 -0
  59. package/lib/adapters/memory/store.d.ts +34 -0
  60. package/lib/adapters/memory/store.js +266 -0
  61. package/lib/adapters/sqlite/backups.d.ts +53 -0
  62. package/lib/adapters/sqlite/backups.js +232 -0
  63. package/lib/adapters/sqlite/blobs.d.ts +14 -0
  64. package/lib/adapters/sqlite/blobs.js +109 -0
  65. package/lib/adapters/sqlite/board.d.ts +77 -0
  66. package/lib/adapters/sqlite/board.js +328 -0
  67. package/lib/adapters/sqlite/database.d.ts +67 -0
  68. package/lib/adapters/sqlite/database.js +285 -0
  69. package/lib/adapters/sqlite/index.d.ts +3 -0
  70. package/lib/adapters/sqlite/index.js +2 -0
  71. package/lib/adapters/sqlite/lock.d.ts +11 -0
  72. package/lib/adapters/sqlite/lock.js +34 -0
  73. package/lib/adapters/sqlite/records.d.ts +54 -0
  74. package/lib/adapters/sqlite/records.js +546 -0
  75. package/lib/adapters/sqlite/schema.d.ts +54 -0
  76. package/lib/adapters/sqlite/schema.js +276 -0
  77. package/lib/adapters/sqlite/store.d.ts +39 -0
  78. package/lib/adapters/sqlite/store.js +385 -0
  79. package/lib/browser/in-page.d.ts +31 -0
  80. package/lib/browser/in-page.js +46 -0
  81. package/lib/browser/old-preview.d.ts +13 -0
  82. package/lib/browser/old-preview.js +80 -0
  83. package/lib/browser/preview-board.d.ts +8 -0
  84. package/lib/browser/preview-board.js +69 -0
  85. package/lib/cli/main.d.ts +7 -0
  86. package/lib/cli/main.js +343 -0
  87. package/lib/cli/orbit.d.ts +8 -0
  88. package/lib/cli/orbit.js +25 -0
  89. package/lib/client/http.d.ts +38 -0
  90. package/lib/client/http.js +334 -0
  91. package/lib/client/local.d.ts +26 -0
  92. package/lib/client/local.js +190 -0
  93. package/lib/client/object-url.d.ts +8 -0
  94. package/lib/client/object-url.js +21 -0
  95. package/lib/client/types.d.ts +268 -0
  96. package/lib/client/types.js +8 -0
  97. package/lib/commands/agent-tools.d.ts +31 -0
  98. package/lib/commands/agent-tools.js +100 -0
  99. package/lib/commands/index.d.ts +56 -0
  100. package/lib/commands/index.js +298 -0
  101. package/lib/commands/parse.d.ts +73 -0
  102. package/lib/commands/parse.js +231 -0
  103. package/lib/commands/present.d.ts +134 -0
  104. package/lib/commands/present.js +198 -0
  105. package/lib/commands/session.d.ts +85 -0
  106. package/lib/commands/session.js +138 -0
  107. package/lib/commands/verbs.d.ts +35 -0
  108. package/lib/commands/verbs.js +842 -0
  109. package/lib/core/actors.d.ts +18 -0
  110. package/lib/core/actors.js +45 -0
  111. package/lib/core/agent.d.ts +128 -0
  112. package/lib/core/agent.js +323 -0
  113. package/lib/core/boards.d.ts +113 -0
  114. package/lib/core/boards.js +288 -0
  115. package/lib/core/bootstrap.d.ts +47 -0
  116. package/lib/core/bootstrap.js +100 -0
  117. package/lib/core/bytes.d.ts +10 -0
  118. package/lib/core/bytes.js +52 -0
  119. package/lib/core/card-paths.d.ts +25 -0
  120. package/lib/core/card-paths.js +44 -0
  121. package/lib/core/context.d.ts +60 -0
  122. package/lib/core/context.js +42 -0
  123. package/lib/core/cross-board.d.ts +81 -0
  124. package/lib/core/cross-board.js +228 -0
  125. package/lib/core/describe.d.ts +21 -0
  126. package/lib/core/describe.js +81 -0
  127. package/lib/core/errors.d.ts +159 -0
  128. package/lib/core/errors.js +160 -0
  129. package/lib/core/files.d.ts +79 -0
  130. package/lib/core/files.js +262 -0
  131. package/lib/core/history.d.ts +20 -0
  132. package/lib/core/history.js +53 -0
  133. package/lib/core/image-types.d.ts +16 -0
  134. package/lib/core/image-types.js +36 -0
  135. package/lib/core/model.d.ts +263 -0
  136. package/lib/core/model.js +34 -0
  137. package/lib/core/nebula-link.d.ts +14 -0
  138. package/lib/core/nebula-link.js +39 -0
  139. package/lib/core/relations.d.ts +58 -0
  140. package/lib/core/relations.js +112 -0
  141. package/lib/core/search.d.ts +38 -0
  142. package/lib/core/search.js +92 -0
  143. package/lib/core/sessions.d.ts +44 -0
  144. package/lib/core/sessions.js +158 -0
  145. package/lib/core/snapshot-schema.d.ts +295 -0
  146. package/lib/core/snapshot-schema.js +82 -0
  147. package/lib/core/snapshots.d.ts +75 -0
  148. package/lib/core/snapshots.js +409 -0
  149. package/lib/core/states.d.ts +25 -0
  150. package/lib/core/states.js +126 -0
  151. package/lib/core/text.d.ts +40 -0
  152. package/lib/core/text.js +113 -0
  153. package/lib/core/tickets.d.ts +69 -0
  154. package/lib/core/tickets.js +311 -0
  155. package/lib/core/trello.d.ts +17 -0
  156. package/lib/core/trello.js +269 -0
  157. package/lib/core/verdicts.d.ts +28 -0
  158. package/lib/core/verdicts.js +92 -0
  159. package/lib/core/views.d.ts +95 -0
  160. package/lib/core/views.js +186 -0
  161. package/lib/http/app.d.ts +46 -0
  162. package/lib/http/app.js +491 -0
  163. package/lib/mcp-server.d.ts +10 -0
  164. package/lib/mcp-server.js +120 -0
  165. package/lib/node/assets.d.ts +7 -0
  166. package/lib/node/assets.js +68 -0
  167. package/lib/node/data-folder.d.ts +21 -0
  168. package/lib/node/data-folder.js +34 -0
  169. package/lib/node/env.d.ts +38 -0
  170. package/lib/node/env.js +49 -0
  171. package/lib/node/host-check.d.ts +24 -0
  172. package/lib/node/host-check.js +53 -0
  173. package/lib/node/host-services.d.ts +9 -0
  174. package/lib/node/host-services.js +150 -0
  175. package/lib/node/local.d.ts +22 -0
  176. package/lib/node/local.js +61 -0
  177. package/lib/node/migrate.d.ts +53 -0
  178. package/lib/node/migrate.js +635 -0
  179. package/lib/node/package.d.ts +7 -0
  180. package/lib/node/package.js +14 -0
  181. package/lib/node/repo-board.d.ts +49 -0
  182. package/lib/node/repo-board.js +245 -0
  183. package/lib/node/serve.d.ts +8 -0
  184. package/lib/node/serve.js +106 -0
  185. package/lib/orbit.d.ts +47 -0
  186. package/lib/orbit.js +44 -0
  187. package/lib/quiet-sqlite-warning.d.ts +1 -0
  188. package/lib/quiet-sqlite-warning.js +18 -0
  189. package/lib/store/blob-path.d.ts +6 -0
  190. package/lib/store/blob-path.js +12 -0
  191. package/lib/store/conformance.d.ts +52 -0
  192. package/lib/store/conformance.js +1449 -0
  193. package/lib/store/search-words.d.ts +10 -0
  194. package/lib/store/search-words.js +19 -0
  195. package/lib/store/types.d.ts +452 -0
  196. package/lib/store/types.js +34 -0
  197. package/llms.txt +63 -0
  198. package/package.json +109 -25
  199. package/src/cli/orbit.js +11 -658
  200. package/src/mcp-server.js +16 -876
  201. package/SKILL-ORBIT.md +0 -151
  202. package/build.mjs +0 -210
  203. package/dist/full/js/app.62905fcb.js +0 -952
  204. package/dist/full/js/app.62905fcb.js.map +0 -7
  205. package/dist/full/styles.4e60d824.css +0 -1
  206. package/docs/AGENT_PROTOCOL.md +0 -424
  207. package/docs/MCP_SETUP.md +0 -54
  208. package/docs/ORBIT_DISPATCH.md +0 -280
  209. package/docs/SS-Orbit-Dark.png +0 -0
  210. package/docs/SS-Orbit-Light.png +0 -0
  211. package/docs/mcp-client.example.json +0 -13
  212. package/docs/setup/orbit-init-wizard.md +0 -174
  213. package/public/apple-touch-icon.png +0 -0
  214. package/public/cursors/live_arrow.cur +0 -0
  215. package/public/cursors/live_link.cur +0 -0
  216. package/public/cursors/live_move.cur +0 -0
  217. package/public/cursors/live_select.cur +0 -0
  218. package/public/cursors/live_unavail.cur +0 -0
  219. package/public/favicon-32x32.png +0 -0
  220. package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
  221. package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
  222. package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
  223. package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
  224. package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
  225. package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
  226. package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
  227. package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
  228. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
  229. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
  230. package/public/fonts/Chakra_Petch/OFL.txt +0 -93
  231. package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
  232. package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
  233. package/public/fonts/Inter/OFL.txt +0 -93
  234. package/public/fonts/Inter/README.txt +0 -118
  235. package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
  236. package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
  237. package/public/fonts/JetBrains_Mono/README.txt +0 -79
  238. package/public/fonts/OpenRing-Regular.ttf +0 -0
  239. package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  240. package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
  241. package/public/fonts/Open_Sans/README.txt +0 -100
  242. package/public/fonts/Orbitron/OFL.txt +0 -93
  243. package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
  244. package/public/fonts/Orbitron/README.txt +0 -68
  245. package/public/fonts/Rajdhani/OFL.txt +0 -93
  246. package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
  247. package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
  248. package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
  249. package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
  250. package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
  251. package/public/fonts/runescape-uf.ttf +0 -0
  252. package/public/index.html +0 -769
  253. package/public/js/actor-labels.js +0 -65
  254. package/public/js/api.js +0 -39
  255. package/public/js/app.js +0 -45
  256. package/public/js/artifact-drawer.js +0 -79
  257. package/public/js/board-export.js +0 -161
  258. package/public/js/board-menu.js +0 -374
  259. package/public/js/card-actions.js +0 -545
  260. package/public/js/config.js +0 -20
  261. package/public/js/create-card.js +0 -190
  262. package/public/js/dom.js +0 -19
  263. package/public/js/drawer.js +0 -273
  264. package/public/js/font-preference.js +0 -229
  265. package/public/js/format.js +0 -433
  266. package/public/js/help-menu.js +0 -31
  267. package/public/js/icons.js +0 -16
  268. package/public/js/kanban.js +0 -694
  269. package/public/js/local-backend.js +0 -1015
  270. package/public/js/main.js +0 -161
  271. package/public/js/modal.js +0 -33
  272. package/public/js/motion-preference.js +0 -64
  273. package/public/js/panel.js +0 -58
  274. package/public/js/router.js +0 -134
  275. package/public/js/search.js +0 -56
  276. package/public/js/settings-tabs.js +0 -7
  277. package/public/js/settings.js +0 -1634
  278. package/public/js/sse.js +0 -159
  279. package/public/js/starscape.js +0 -332
  280. package/public/js/state.js +0 -129
  281. package/public/js/text-editing.js +0 -128
  282. package/public/js/theme-preference.js +0 -95
  283. package/public/js/ticket-detail.js +0 -1516
  284. package/public/js/ticket-typeahead.js +0 -49
  285. package/public/js/toast.js +0 -89
  286. package/public/js/topbar-notes.js +0 -155
  287. package/public/js/transport.js +0 -22
  288. package/public/js/typeahead.js +0 -206
  289. package/public/js/unread.js +0 -121
  290. package/public/js/update-check.js +0 -58
  291. package/public/js/url-routes.js +0 -94
  292. package/public/orbits-favicon.png +0 -0
  293. package/public/orbits-logo-bg-wide.png +0 -0
  294. package/public/orbits-logo-bg.png +0 -0
  295. package/public/orbits-logo.png +0 -0
  296. package/public/styles.css +0 -7709
  297. package/src/core/agent.js +0 -387
  298. package/src/core/agents-md.js +0 -109
  299. package/src/core/artifacts.js +0 -149
  300. package/src/core/attachments.js +0 -238
  301. package/src/core/auth.js +0 -85
  302. package/src/core/backups.js +0 -142
  303. package/src/core/board.js +0 -148
  304. package/src/core/boards.js +0 -409
  305. package/src/core/bootstrap.js +0 -170
  306. package/src/core/css-bundle.js +0 -30
  307. package/src/core/db.js +0 -315
  308. package/src/core/delete-board.js +0 -163
  309. package/src/core/dispatch.js +0 -1007
  310. package/src/core/events.js +0 -314
  311. package/src/core/http.js +0 -115
  312. package/src/core/migrate.js +0 -198
  313. package/src/core/paths.js +0 -55
  314. package/src/core/provision-repo-board.js +0 -119
  315. package/src/core/queries.js +0 -246
  316. package/src/core/registry.js +0 -211
  317. package/src/core/relations.js +0 -163
  318. package/src/core/review-verdicts.js +0 -138
  319. package/src/core/router.js +0 -556
  320. package/src/core/search.js +0 -183
  321. package/src/core/seed.js +0 -234
  322. package/src/core/snapshots.js +0 -692
  323. package/src/core/states.js +0 -167
  324. package/src/core/system-picker.js +0 -109
  325. package/src/core/tickets.js +0 -400
  326. package/src/core/util.js +0 -106
  327. package/src/core/version-check.js +0 -154
  328. package/src/mcp/orbit-client.js +0 -287
  329. package/src/server.js +0 -115
@@ -0,0 +1,385 @@
1
+ /**
2
+ * The SQLite adapter: Orbit's storage port on `node:sqlite`, over a data
3
+ * folder.
4
+ *
5
+ * registry.db the list of boards and the settings
6
+ * boards/<folder>/board.db one board's records
7
+ * boards/<folder>/attachments/... its images (the blob store)
8
+ * boards/<folder>/artifacts/... its markdown
9
+ * backups/ copies (VACUUM INTO), with each board's files linked beside them
10
+ *
11
+ * The schema is ./schema.ts. A folder whose files lack v2's version (Orbit
12
+ * 1.x, or 2.0 before its rebuild) is refused untouched, with
13
+ * `data_needs_migration`. Several stores, in one process or several, can open
14
+ * one folder at once: SQLite's locks keep their writes one at a time, and
15
+ * each hears the others' changes.
16
+ */
17
+ import { existsSync, mkdirSync, readdirSync, rmdirSync, rmSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { OrbitError, isOrbitError } from "../../core/errors.js";
20
+ import { createSqliteBackups } from "./backups.js";
21
+ import { createSqliteBlobStore } from "./blobs.js";
22
+ import { BoardFiles } from "./board.js";
23
+ import { checkFile, Connection, createFile, untilFree } from "./database.js";
24
+ import { Lock } from "./lock.js";
25
+ import { CROSS, toRecord } from "./records.js";
26
+ import { BOARD_SCHEMA, REGISTRY_SCHEMA } from "./schema.js";
27
+ const LISTING = "id, slug, name, folder, repo_url, default_branch, last_active_at, created_at, updated_at";
28
+ const REGISTRY = {
29
+ list: `SELECT ${LISTING} FROM boards ORDER BY name, seq`,
30
+ get: `SELECT ${LISTING} FROM boards WHERE id = ?`,
31
+ bySlug: `SELECT ${LISTING} FROM boards WHERE slug = ?`,
32
+ latest: `SELECT ${LISTING} FROM boards ORDER BY last_active_at DESC, seq DESC LIMIT 1`,
33
+ slugTaken: "SELECT 1 AS taken FROM boards WHERE slug = ? AND id <> ?",
34
+ folderTaken: "SELECT 1 AS taken FROM boards WHERE folder = ?",
35
+ insert: `INSERT INTO boards (${LISTING}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
36
+ touch: "UPDATE boards SET last_active_at = ?, updated_at = ? WHERE id = ?",
37
+ follow: "UPDATE boards SET slug = ?, name = ?, repo_url = ?, default_branch = ?, updated_at = ? WHERE id = ?",
38
+ remove: "DELETE FROM boards WHERE id = ? RETURNING folder",
39
+ setting: "SELECT value FROM settings WHERE key = ?",
40
+ setSetting: "INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value"
41
+ };
42
+ const TABLE_OF = { state: "states", ticket: "tickets", entry: "board_entries", relation: "relations" };
43
+ /** A board's folder name, from the slug it was made with: letters, digits, `.`, `_` and `-`. */
44
+ function folderName(slug) {
45
+ const name = slug.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[-.]+|-+$/g, "").slice(0, 80);
46
+ return name || "board";
47
+ }
48
+ /**
49
+ * Opens a store over `dataDir`, making the folder and its registry when
50
+ * they're missing. Rejects with `data_needs_migration` for a folder of older
51
+ * data, which it leaves as it was.
52
+ */
53
+ export function openSqliteStore(options) {
54
+ return new Promise((resolve) => {
55
+ resolve(open(options));
56
+ });
57
+ }
58
+ function open(options) {
59
+ const { dataDir } = options;
60
+ const busyMs = options.busyTimeoutMs ?? 5000;
61
+ const registryPath = join(dataDir, "registry.db");
62
+ const boardsDir = join(dataDir, "boards");
63
+ // Checked before anything is made, so a folder of older data is left exactly as it was.
64
+ if (!checkFile(registryPath)) {
65
+ mkdirSync(dataDir, { recursive: true });
66
+ createFile(registryPath, REGISTRY_SCHEMA);
67
+ checkFile(registryPath);
68
+ }
69
+ mkdirSync(boardsDir, { recursive: true });
70
+ const regWriter = new Connection(registryPath);
71
+ const regReader = new Connection(registryPath, { queryOnly: true });
72
+ const registryLock = new Lock();
73
+ /** Each board's folder, once seen; kept after a board is removed, so its blobs can still be removed. */
74
+ const folders = new Map();
75
+ const opened = new Map();
76
+ let closed = false;
77
+ const deadline = () => Date.now() + busyMs;
78
+ const live = () => {
79
+ if (closed)
80
+ throw new OrbitError("storage_failed", { message: "This store is closed." });
81
+ };
82
+ const listing = (row) => {
83
+ const folder = String(row.folder);
84
+ folders.set(String(row.id), folder);
85
+ return {
86
+ id: String(row.id),
87
+ slug: String(row.slug),
88
+ name: String(row.name),
89
+ db_path: join(boardsDir, folder, "board.db"),
90
+ repo_url: String(row.repo_url),
91
+ default_branch: String(row.default_branch),
92
+ last_active_at: row.last_active_at === null ? null : String(row.last_active_at),
93
+ created_at: String(row.created_at),
94
+ updated_at: String(row.updated_at)
95
+ };
96
+ };
97
+ const find = (sql, key) => {
98
+ live();
99
+ const row = regReader.get(sql, [key]);
100
+ return row ? listing(row) : null;
101
+ };
102
+ const listed = () => {
103
+ live();
104
+ return regReader.all(REGISTRY.list).map(listing);
105
+ };
106
+ /** A registry write on its own, in turn with the others. */
107
+ const registryWrite = (attempt) => registryLock.run(() => untilFree(attempt, deadline()));
108
+ const host = {
109
+ busyMs,
110
+ slugTaken: (slug, exceptId) => regReader.get(REGISTRY.slugTaken, [slug, exceptId]) !== undefined,
111
+ async follow(board) {
112
+ const release = await registryLock.acquire();
113
+ try {
114
+ await untilFree(() => {
115
+ regWriter.exec("BEGIN IMMEDIATE");
116
+ }, deadline());
117
+ try {
118
+ regWriter.run(REGISTRY.follow, [board.slug, board.name, board.repo_url, board.default_branch, board.updated_at, board.id]);
119
+ }
120
+ catch (error) {
121
+ regWriter.rollback();
122
+ throw error;
123
+ }
124
+ }
125
+ catch (error) {
126
+ release();
127
+ throw error;
128
+ }
129
+ return {
130
+ commit() {
131
+ try {
132
+ regWriter.exec("COMMIT");
133
+ }
134
+ finally {
135
+ release();
136
+ }
137
+ },
138
+ rollback() {
139
+ regWriter.rollback();
140
+ release();
141
+ }
142
+ };
143
+ }
144
+ };
145
+ /** The board's open files: opened once per store, and again after the board was made anew. */
146
+ async function filesOf(entry) {
147
+ const pending = opened.get(entry.id);
148
+ if (pending) {
149
+ const files = await pending.catch(() => null);
150
+ if (files?.isLive())
151
+ return files;
152
+ if (opened.get(entry.id) === pending)
153
+ opened.delete(entry.id);
154
+ }
155
+ const opening = BoardFiles.open(host, entry.id, entry.db_path, entry.slug);
156
+ opened.set(entry.id, opening);
157
+ try {
158
+ return await opening;
159
+ }
160
+ catch (error) {
161
+ if (opened.get(entry.id) === opening)
162
+ opened.delete(entry.id);
163
+ throw error;
164
+ }
165
+ }
166
+ const boardStores = new WeakMap();
167
+ const storeOf = (files) => {
168
+ let store = boardStores.get(files);
169
+ if (!store) {
170
+ store = files.store();
171
+ boardStores.set(files, store);
172
+ }
173
+ return store;
174
+ };
175
+ /** A folder no board uses and nothing is in. */
176
+ function freeFolder(slug) {
177
+ const base = folderName(slug);
178
+ for (let n = 1;; n += 1) {
179
+ const name = n === 1 ? base : `${base}-${String(n)}`;
180
+ if (regWriter.get(REGISTRY.folderTaken, [name]))
181
+ continue;
182
+ const path = join(boardsDir, name);
183
+ if (existsSync(path) && readdirSync(path).length > 0)
184
+ continue;
185
+ return name;
186
+ }
187
+ }
188
+ const boards = {
189
+ list: () => Promise.resolve(listed()),
190
+ get: (id) => Promise.resolve(find(REGISTRY.get, id)),
191
+ bySlug: (slug) => Promise.resolve(find(REGISTRY.bySlug, slug)),
192
+ latest() {
193
+ live();
194
+ const row = regReader.get(REGISTRY.latest);
195
+ return Promise.resolve(row ? listing(row) : null);
196
+ },
197
+ create(board, seed) {
198
+ return registryLock.run(async () => {
199
+ live();
200
+ await untilFree(() => {
201
+ regWriter.exec("BEGIN IMMEDIATE");
202
+ }, deadline());
203
+ let folderPath = null;
204
+ let files = null;
205
+ try {
206
+ if (regWriter.get(REGISTRY.get, [board.id]))
207
+ throw new OrbitError("duplicate_id");
208
+ if (regWriter.get(REGISTRY.bySlug, [board.slug]))
209
+ throw new OrbitError("board_slug_taken");
210
+ const folder = freeFolder(board.slug);
211
+ folderPath = join(boardsDir, folder);
212
+ mkdirSync(folderPath, { recursive: true });
213
+ const dbPath = join(folderPath, "board.db");
214
+ createFile(dbPath, BOARD_SCHEMA);
215
+ files = await BoardFiles.open(host, board.id, dbPath, board.slug);
216
+ const made = await files.seed(board, seed);
217
+ regWriter.run(REGISTRY.insert, [made.id, made.slug, made.name, folder, made.repo_url, made.default_branch, board.created_at, made.created_at, made.updated_at]);
218
+ regWriter.exec("COMMIT");
219
+ opened.set(board.id, Promise.resolve(files));
220
+ const entry = regReader.get(REGISTRY.get, [board.id]);
221
+ if (!entry)
222
+ throw new OrbitError("storage_failed");
223
+ return listing(entry);
224
+ }
225
+ catch (error) {
226
+ regWriter.rollback();
227
+ files?.markRemoved();
228
+ if (folderPath)
229
+ rmSync(folderPath, { recursive: true, force: true });
230
+ throw error;
231
+ }
232
+ });
233
+ },
234
+ async touch(id, at) {
235
+ live();
236
+ const { changes } = await registryWrite(() => regWriter.run(REGISTRY.touch, [at, at, id]));
237
+ if (changes === 0)
238
+ throw new OrbitError("board_not_found");
239
+ },
240
+ async remove(id, options = {}) {
241
+ live();
242
+ const entry = find(REGISTRY.get, id);
243
+ if (!entry)
244
+ throw new OrbitError("board_not_found");
245
+ const files = await opened.get(id)?.catch(() => null);
246
+ const unlist = async () => {
247
+ const removed = [];
248
+ if (!options.keepData) {
249
+ // The files go before the board leaves the list: a delete stopped by a
250
+ // file another program holds leaves the board listed, to be deleted again.
251
+ // This store lets go of them first, since an open handle keeps a file on Windows.
252
+ files?.markRemoved();
253
+ opened.delete(id);
254
+ try {
255
+ for (const suffix of ["", "-wal", "-shm", "-journal"]) {
256
+ const path = `${entry.db_path}${suffix}`;
257
+ if (!existsSync(path))
258
+ continue;
259
+ rmSync(path, REMOVE_FILE);
260
+ removed.push(path);
261
+ }
262
+ }
263
+ catch (error) {
264
+ throw filesBusy(error, entry.slug);
265
+ }
266
+ }
267
+ const row = await registryWrite(() => regWriter.get(REGISTRY.remove, [id]));
268
+ if (!row)
269
+ throw new OrbitError("board_not_found");
270
+ if (options.keepData) {
271
+ files?.markRemoved();
272
+ opened.delete(id);
273
+ return { removed };
274
+ }
275
+ try {
276
+ rmdirSync(join(boardsDir, String(row.folder)));
277
+ }
278
+ catch {
279
+ // Its images and markdown are still there: the blob store's to remove.
280
+ }
281
+ return { removed };
282
+ };
283
+ return files ? files.exclusive(unlist) : unlist();
284
+ }
285
+ };
286
+ const settings = {
287
+ get(key) {
288
+ live();
289
+ const row = regReader.get(REGISTRY.setting, [key]);
290
+ return Promise.resolve(row ? String(row.value) : null);
291
+ },
292
+ async set(key, value) {
293
+ live();
294
+ await registryWrite(() => regWriter.run(REGISTRY.setSetting, [key, value]));
295
+ }
296
+ };
297
+ /** A board's folder: what this store has seen, else the registry's; null for a board it never had. */
298
+ const folderOf = (boardId) => {
299
+ if (!folders.has(boardId))
300
+ find(REGISTRY.get, boardId);
301
+ const folder = folders.get(boardId);
302
+ return folder === undefined ? null : join(boardsDir, folder);
303
+ };
304
+ return {
305
+ boards,
306
+ settings,
307
+ blobs: createSqliteBlobStore(folderOf),
308
+ backups: createSqliteBackups({
309
+ dataDir,
310
+ retention: options.backupRetention ?? 30,
311
+ busyMs,
312
+ now: options.now ?? (() => new Date()),
313
+ board(id) {
314
+ const entry = find(REGISTRY.get, id);
315
+ return entry ? { dbPath: entry.db_path, slug: entry.slug, name: entry.name } : null;
316
+ }
317
+ }),
318
+ async board(id) {
319
+ live();
320
+ const pending = opened.get(id);
321
+ const known = pending ? await pending.catch(() => null) : null;
322
+ if (known?.isLive())
323
+ return storeOf(known);
324
+ const entry = find(REGISTRY.get, id);
325
+ if (!entry)
326
+ throw new OrbitError("board_not_found");
327
+ return storeOf(await filesOf(entry));
328
+ },
329
+ async locate(kind, id, hint) {
330
+ live();
331
+ const order = listed().sort((a, b) => Number(b.id === hint) - Number(a.id === hint));
332
+ for (const entry of order) {
333
+ const files = await filesOf(entry);
334
+ if (files.query(`SELECT 1 AS here FROM ${TABLE_OF[kind]} WHERE id = ? LIMIT 1`, [id]).length > 0)
335
+ return entry;
336
+ }
337
+ return null;
338
+ },
339
+ async relationsInto(boardId) {
340
+ live();
341
+ const found = [];
342
+ for (const entry of listed()) {
343
+ // A board whose file is gone (or won't open) links to nothing, as cross-board reads treat it: one such board never stops every other card's drawer.
344
+ const files = await filesOf(entry).catch((error) => {
345
+ if (isOrbitError(error))
346
+ return null;
347
+ throw error;
348
+ });
349
+ if (!files)
350
+ continue;
351
+ for (const row of files.query(`${CROSS.select} WHERE target_board_id = ? ORDER BY created_at, seq`, [boardId])) {
352
+ found.push({ board_id: entry.id, relation: toRecord(CROSS, row, entry.id) });
353
+ }
354
+ }
355
+ return found;
356
+ },
357
+ async close() {
358
+ if (closed)
359
+ return;
360
+ closed = true;
361
+ for (const pending of opened.values()) {
362
+ const files = await pending.catch(() => null);
363
+ await files?.close();
364
+ }
365
+ opened.clear();
366
+ await registryLock.run(() => {
367
+ regWriter.close();
368
+ regReader.close();
369
+ return Promise.resolve();
370
+ });
371
+ }
372
+ };
373
+ }
374
+ /** Windows refuses to remove a file another program has open, often only for a moment. */
375
+ const REMOVE_FILE = process.platform === "win32" ? { force: true, maxRetries: 10, retryDelay: 100 } : { force: true };
376
+ /** A file another program holds becomes `board_files_busy`, which says what to do; anything else is a storage fault. */
377
+ function filesBusy(error, slug) {
378
+ const code = error?.code;
379
+ if (code !== "EBUSY" && code !== "EPERM" && code !== "ENOTEMPTY")
380
+ return new OrbitError("storage_failed", { cause: error });
381
+ return new OrbitError("board_files_busy", {
382
+ message: `Orbit couldn't remove the files of board "${slug}": another program has them open. On Windows that is usually an Orbit server, an MCP helper, an AI app, an editor or a terminal. Close it, then delete the board again from Settings.`,
383
+ cause: error
384
+ });
385
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The browser edition: Orbit's own core in the page, over IndexedDB, with
3
+ * no server. `openBrowserOrbit()` opens the store, Orbit and its HTTP app,
4
+ * and gives a `fetch` that answers the JSON API in the page (app.fetch) and
5
+ * an OrbitClient over it. The web app sends its requests there as it would
6
+ * to `orbit run`; the event stream works the same way, through app.fetch.
7
+ *
8
+ * What a browser can't do stays off: agents (MCP), the folder picker,
9
+ * self-update. The version is the build's, with no check for a newer one.
10
+ */
11
+ import type { OrbitClient } from "../client/types.ts";
12
+ import type { OrbitApp } from "../http/app.ts";
13
+ import type { Orbit } from "../orbit.ts";
14
+ export interface BrowserOrbit {
15
+ orbit: Orbit;
16
+ app: OrbitApp;
17
+ /** The JSON API, answered in this page. */
18
+ fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
19
+ /** The OrbitClient over that fetch: images as object URLs, changes through the event stream. */
20
+ client: OrbitClient;
21
+ close(): Promise<void>;
22
+ }
23
+ export interface BrowserOrbitOptions {
24
+ /** The build's version, for Help and /api/version. */
25
+ version: string;
26
+ /** The IndexedDB database. Default: `orbit`. */
27
+ name?: string;
28
+ /** Make the welcome board when there are no boards. Default: true. */
29
+ seed?: boolean;
30
+ }
31
+ export declare function openBrowserOrbit(options: BrowserOrbitOptions): Promise<BrowserOrbit>;
@@ -0,0 +1,46 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * The browser edition: Orbit's own core in the page, over IndexedDB, with
4
+ * no server. `openBrowserOrbit()` opens the store, Orbit and its HTTP app,
5
+ * and gives a `fetch` that answers the JSON API in the page (app.fetch) and
6
+ * an OrbitClient over it. The web app sends its requests there as it would
7
+ * to `orbit run`; the event stream works the same way, through app.fetch.
8
+ *
9
+ * What a browser can't do stays off: agents (MCP), the folder picker,
10
+ * self-update. The version is the build's, with no check for a newer one.
11
+ */
12
+ import { openIndexedDbStore } from "../adapters/indexeddb/index.js";
13
+ import { createLocalLocks, webLocks } from "../adapters/indexeddb/locks.js";
14
+ import { createHttpClient } from "../client/http.js";
15
+ import { createOrbitApp } from "../http/app.js";
16
+ import { createOrbit } from "../orbit.js";
17
+ import { bringOldPreview } from "./old-preview.js";
18
+ import { seedPreviewBoard } from "./preview-board.js";
19
+ export async function openBrowserOrbit(options) {
20
+ const locks = webLocks() ?? createLocalLocks();
21
+ const store = await openIndexedDbStore({ locks, ...(options.name ? { name: options.name } : {}) });
22
+ const version = {
23
+ current: options.version,
24
+ deployMode: "browser",
25
+ selfUpdate: false,
26
+ repoUrl: "",
27
+ changesUrl: "",
28
+ enabled: false,
29
+ latest: null,
30
+ updateAvailable: false,
31
+ error: null,
32
+ checkedAt: null
33
+ };
34
+ const orbit = createOrbit({ store, blobs: store.blobs, host: { version: () => Promise.resolve(version) } });
35
+ const app = createOrbitApp(orbit, { version: options.version });
36
+ if (options.seed !== false) {
37
+ // The old preview's board first (once per origin), so a person who used it finds it instead of a welcome board.
38
+ await locks.request("orbit-browser-edition:seed", async () => {
39
+ await bringOldPreview(orbit).catch(() => null);
40
+ });
41
+ await seedPreviewBoard(orbit, locks);
42
+ }
43
+ const inPage = (input, init) => app.fetch(new Request(input, init));
44
+ const client = createHttpClient({ baseUrl: location.origin, fetch: inPage, images: "object" });
45
+ return { orbit, app, fetch: inPage, client, close: () => orbit.close() };
46
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The board of the old browser preview (1.x and 2.0 before its rebuild kept
3
+ * it in this origin's `starscape-orbit-preview` database), brought into the
4
+ * browser edition once, so a person who tried Orbit here finds their board.
5
+ * Its database is opened at the version it has and read in one read-only
6
+ * transaction, so it stays as it was. A board nobody changed (the preview's
7
+ * own welcome board, as it was made) isn't brought: the new edition has its
8
+ * own. Whatever happens, it is decided once per origin.
9
+ */
10
+ import type { Orbit } from "../orbit.ts";
11
+ export declare const OLD_PREVIEW_DB = "starscape-orbit-preview";
12
+ /** Brings the old preview's board over, once per origin; says what it did (or null when it was decided before). */
13
+ export declare function bringOldPreview(orbit: Orbit): Promise<string | null>;
@@ -0,0 +1,80 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * The board of the old browser preview (1.x and 2.0 before its rebuild kept
4
+ * it in this origin's `starscape-orbit-preview` database), brought into the
5
+ * browser edition once, so a person who tried Orbit here finds their board.
6
+ * Its database is opened at the version it has and read in one read-only
7
+ * transaction, so it stays as it was. A board nobody changed (the preview's
8
+ * own welcome board, as it was made) isn't brought: the new edition has its
9
+ * own. Whatever happens, it is decided once per origin.
10
+ */
11
+ import { localOwnerActor } from "../core/actors.js";
12
+ import { OLD_PREVIEW_FORMAT, fromOldPreview, importKeepingBoard } from "../core/snapshots.js";
13
+ export const OLD_PREVIEW_DB = "starscape-orbit-preview";
14
+ const DONE = "old_preview";
15
+ const STORES = ["boards", "states", "labels", "tickets", "ticket_labels", "comments", "relations", "board_entries", "events"];
16
+ /** The old database's stores, read whole; null when this origin has none. */
17
+ async function readOldPreview() {
18
+ if (typeof indexedDB === "undefined" || typeof indexedDB.databases !== "function")
19
+ return null;
20
+ // Opening a database that isn't there would make it: look first.
21
+ if (!(await indexedDB.databases()).some((db) => db.name === OLD_PREVIEW_DB))
22
+ return null;
23
+ const db = await new Promise((resolve, reject) => {
24
+ const request = indexedDB.open(OLD_PREVIEW_DB);
25
+ request.onsuccess = () => {
26
+ resolve(request.result);
27
+ };
28
+ request.onerror = () => {
29
+ reject(request.error ?? new Error("The old preview's database didn't open."));
30
+ };
31
+ });
32
+ try {
33
+ const names = STORES.filter((name) => db.objectStoreNames.contains(name));
34
+ if (!names.length)
35
+ return null;
36
+ const tx = db.transaction(names, "readonly");
37
+ const read = await Promise.all(names.map((name) => new Promise((resolve, reject) => {
38
+ const request = tx.objectStore(name).getAll();
39
+ request.onsuccess = () => {
40
+ resolve([name, request.result]);
41
+ };
42
+ request.onerror = () => {
43
+ reject(request.error ?? new Error(`The old preview's ${name} didn't read.`));
44
+ };
45
+ })));
46
+ return Object.fromEntries(read);
47
+ }
48
+ finally {
49
+ db.close();
50
+ }
51
+ }
52
+ /** True when a person changed the board after the preview made it: any record newer than the board. */
53
+ function changedByAPerson(stores, board) {
54
+ const made = typeof board.created_at === "string" ? board.created_at : "";
55
+ if ((stores.events ?? []).length)
56
+ return true;
57
+ return STORES.some((name) => (stores[name] ?? []).some((row) => [row.created_at, row.updated_at, row.struck_at, row.archived_at].some((at) => typeof at === "string" && at > made)));
58
+ }
59
+ /** Brings the old preview's board over, once per origin; says what it did (or null when it was decided before). */
60
+ export async function bringOldPreview(orbit) {
61
+ const c = orbit.context(localOwnerActor());
62
+ if (await c.store.settings.get(DONE))
63
+ return null;
64
+ const stores = await readOldPreview().catch(() => null);
65
+ const board = stores?.boards?.[0];
66
+ let outcome = "none";
67
+ if (stores && board && changedByAPerson(stores, board)) {
68
+ let slug = typeof board.slug === "string" && board.slug ? board.slug : "preview";
69
+ for (let i = 2; await c.store.boards.bySlug(slug); i += 1)
70
+ slug = `${typeof board.slug === "string" && board.slug ? board.slug : "preview"}-${i}`;
71
+ const id = typeof board.id === "string" && board.id && !(await c.store.boards.get(board.id)) ? board.id : c.ids.next();
72
+ await importKeepingBoard(c, fromOldPreview({ schema: OLD_PREVIEW_FORMAT, board, ...stores }), { id, slug });
73
+ outcome = `brought:${slug}`;
74
+ }
75
+ else if (stores && board) {
76
+ outcome = "unchanged";
77
+ }
78
+ await c.store.settings.set(DONE, JSON.stringify({ outcome, at: c.clock.now() }));
79
+ return outcome;
80
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The board a newcomer finds in the browser edition: lanes, a few cards
3
+ * that show the way around, and a note. Made once, through the core, by
4
+ * whichever tab gets there first.
5
+ */
6
+ import type { Locks } from "../adapters/indexeddb/locks.ts";
7
+ import type { Orbit } from "../orbit.ts";
8
+ export declare function seedPreviewBoard(orbit: Orbit, locks: Locks): Promise<void>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The board a newcomer finds in the browser edition: lanes, a few cards
3
+ * that show the way around, and a note. Made once, through the core, by
4
+ * whichever tab gets there first.
5
+ */
6
+ import { localOwnerActor } from "../core/actors.js";
7
+ const LANES = [
8
+ ["Backlog", null],
9
+ ["Todo", null],
10
+ ["In Progress", "in_progress"],
11
+ ["Review", "review"],
12
+ ["Done", "done"],
13
+ ["Cancelled", null]
14
+ ];
15
+ export async function seedPreviewBoard(orbit, locks) {
16
+ const client = orbit.client({ ...localOwnerActor(), id: "system", name: "system" });
17
+ await locks.request("orbit-browser-edition:seed", async () => {
18
+ if ((await client.boards.list()).boards.length)
19
+ return;
20
+ const board = await client.boards.create({ name: "Preview Board", slug: "preview", ai_enabled: false, states: LANES });
21
+ await client.boards.update(board.slug, {
22
+ project_notes: "This preview lives entirely in your browser (IndexedDB). Use it to try the flow; export the snapshot from Settings → Data when you want to move to a real Orbit install.",
23
+ agent_instructions: "Preview edition has no agent or MCP integration. Install the full Orbit app with npm i -g @starlove/orbit, then run orbit run and create or import a board."
24
+ });
25
+ const hint = { board_id: board.id };
26
+ const lanes = (await client.bootstrap(hint)).states;
27
+ const lane = (name) => lanes.find((state) => state.name === name)?.id;
28
+ const epic = await client.tickets.create({
29
+ ...hint,
30
+ title: "Try the preview kanban",
31
+ description: "This onboarding card is for the in-browser preview. Drag it across lanes, edit fields, add comments: it all stays in this browser only.",
32
+ type: "epic",
33
+ ai_plan: "1. Drag this card to In Progress.\n2. Add a comment.\n3. Export the snapshot from Settings → Data.",
34
+ state_id: lane("Todo"),
35
+ priority: 4,
36
+ labels: ["onboarding", "needs-decomposition"]
37
+ });
38
+ await client.tickets.create({
39
+ ...hint,
40
+ title: "Edit a card and watch it persist",
41
+ description: "Click into a card, change its title, refresh: your edit survives because it lives in IndexedDB.",
42
+ type: "feature",
43
+ parent_ticket_id: epic.id,
44
+ state_id: lane("Todo"),
45
+ priority: 3,
46
+ labels: ["onboarding"]
47
+ });
48
+ await client.tickets.create({
49
+ ...hint,
50
+ title: "Move me to In Progress",
51
+ description: "Drag this card to the In Progress lane, then to Review.",
52
+ type: "task",
53
+ parent_ticket_id: epic.id,
54
+ state_id: lane("Backlog"),
55
+ priority: 2,
56
+ labels: ["onboarding"]
57
+ });
58
+ await client.tickets.create({
59
+ ...hint,
60
+ title: "Export your snapshot when ready",
61
+ description: "Settings → Data → Export downloads a JSON snapshot you can import into a real Orbit install.",
62
+ type: "task",
63
+ state_id: lane("Backlog"),
64
+ priority: 1,
65
+ labels: ["onboarding"]
66
+ });
67
+ await client.comments.create(epic.id, { body: "Welcome to the Orbit preview. Everything lives in your browser.", kind: "note" }, hint);
68
+ });
69
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Orbit CLI, loaded by the bin (./orbit.ts): `orbit run` serves the app and the API, `orbit mcp` serves agents
3
+ * over stdio, and any other first word is a board command (`orbit view`).
4
+ * Each command loads only what it runs: Hono for `orbit run`, the MCP SDK for
5
+ * `orbit mcp`, neither for a board command.
6
+ */
7
+ import "../quiet-sqlite-warning.ts";