@starlove/orbit 1.4.1 → 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.4e6fbe20.js +0 -952
  204. package/dist/full/js/app.4e6fbe20.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 -230
  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
@@ -1,314 +0,0 @@
1
- // Event recording and SSE fan-out. `recordEvent` is always called with the
2
- // per-board `db` and the owning `boardId`, so events get written to the
3
- // right per-board DB and SSE listeners only see events for the board they
4
- // subscribed to.
5
- //
6
- // Same-process HTTP mutations can broadcast immediately. MCP/CLI tools mutate
7
- // the same per-board SQLite DB from separate Node processes, so the web server
8
- // also keeps a lightweight per-board watcher while SSE clients are connected.
9
- // Watchers use SQLite `PRAGMA data_version` as the cheap "did another
10
- // connection commit?" check, then drain semantic rows from the `events` table.
11
-
12
- import { localOwnerActor } from "./auth.js";
13
- import { getBoardByRegistryId, openBoardDb } from "./registry.js";
14
- import { sendJson, setCors } from "./http.js";
15
- import { id, now } from "./util.js";
16
-
17
- const sseClients = new Set();
18
- const boardWatchers = new Map();
19
- const DEFAULT_SSE_POLL_MS = 500;
20
- const MIN_SSE_POLL_MS = 25;
21
- const MAX_SSE_POLL_MS = 30_000;
22
- const EVENT_BATCH_LIMIT = 200;
23
- // If a client's socket buffers more than this, it's not keeping up. We end
24
- // the stream so the browser reconnects and replays from Last-Event-ID rather
25
- // than letting Node's write buffer grow without bound.
26
- const SSE_MAX_BUFFERED_BYTES = 1_000_000;
27
-
28
- // Events written inside a tx() are not visible to other connections until
29
- // COMMIT, so broadcasting them immediately would tell SSE listeners about a
30
- // mutation that might still roll back. db.tx() brackets each transaction with
31
- // __enterEventTx/__settleEventTx; while depth > 0 we queue and only fan out
32
- // (or drop, on rollback) once the outermost transaction settles.
33
- let eventTxDepth = 0;
34
- const bufferedBroadcasts = [];
35
-
36
- export function __enterEventTx() {
37
- eventTxDepth += 1;
38
- }
39
-
40
- export function __settleEventTx(committed) {
41
- eventTxDepth -= 1;
42
- if (eventTxDepth > 0) return;
43
- const queued = bufferedBroadcasts.splice(0, bufferedBroadcasts.length);
44
- if (!committed) return;
45
- for (const item of queued) {
46
- broadcastSSE(item.payload);
47
- advanceWatcherCursorByRowid(item.boardId, item.rowid);
48
- }
49
- }
50
-
51
- export function recordEvent(db, boardId, type, ticketId, actor, body) {
52
- const eventId = id();
53
- const timestamp = now();
54
- const result = db.prepare(
55
- "INSERT INTO events (id, ticket_id, actor, type, body_json, created_at) VALUES (?, ?, ?, ?, ?, ?)"
56
- ).run(eventId, ticketId, actor, type, JSON.stringify(body || {}), timestamp);
57
-
58
- const payload = {
59
- id: eventId,
60
- type,
61
- ticket_id: ticketId,
62
- board_id: boardId,
63
- actor,
64
- data: body || {},
65
- timestamp
66
- };
67
-
68
- if (eventTxDepth > 0) {
69
- bufferedBroadcasts.push({ payload, boardId, rowid: result.lastInsertRowid });
70
- return;
71
- }
72
- broadcastSSE(payload);
73
- advanceWatcherCursorByRowid(boardId, result.lastInsertRowid);
74
- }
75
-
76
- export function startSSEStream(req, res, url) {
77
- const actor = localOwnerActor();
78
-
79
- const boardId = url.searchParams.get("board_id") || null;
80
- let board = null;
81
- if (boardId) {
82
- board = getBoardByRegistryId(boardId);
83
- if (!board) {
84
- sendJson(res, 404, { error: "board_not_found" });
85
- return;
86
- }
87
- }
88
-
89
- const lastEventId = req.headers["last-event-id"] || url.searchParams.get("last_event_id") || null;
90
- const watcher = board ? getOrCreateBoardWatcher(board) : null;
91
-
92
- setCors(res);
93
- res.writeHead(200, {
94
- "Content-Type": "text/event-stream",
95
- "Cache-Control": "no-cache",
96
- Connection: "keep-alive",
97
- "X-Accel-Buffering": "no"
98
- });
99
- res.write(":ok\n\n");
100
-
101
- if (lastEventId && board) {
102
- replayEventsForBoard(res, board, lastEventId);
103
- }
104
-
105
- const heartbeat = setInterval(() => {
106
- if (!res.writableEnded) res.write(":heartbeat\n\n");
107
- }, 30_000);
108
-
109
- const client = { res, boardId, boardRow: board, actor, heartbeat, watcher };
110
- sseClients.add(client);
111
- if (watcher) watcher.clients.add(client);
112
-
113
- req.on("close", () => {
114
- clearInterval(heartbeat);
115
- sseClients.delete(client);
116
- if (watcher) {
117
- watcher.clients.delete(client);
118
- stopBoardWatcherIfIdle(watcher);
119
- }
120
- });
121
- }
122
-
123
- function getOrCreateBoardWatcher(boardRow) {
124
- const existing = boardWatchers.get(boardRow.id);
125
- if (existing) {
126
- existing.boardRow = boardRow;
127
- return existing;
128
- }
129
-
130
- const watcher = {
131
- boardId: boardRow.id,
132
- boardRow,
133
- clients: new Set(),
134
- dataVersion: dataVersionForBoard(boardRow),
135
- cursor: latestEventCursor(boardRow),
136
- timer: null
137
- };
138
- watcher.timer = setInterval(() => pollBoardWatcher(watcher), ssePollIntervalMs());
139
- watcher.timer.unref?.();
140
- boardWatchers.set(boardRow.id, watcher);
141
- return watcher;
142
- }
143
-
144
- function stopBoardWatcherIfIdle(watcher) {
145
- if (watcher.clients.size > 0) return;
146
- clearInterval(watcher.timer);
147
- boardWatchers.delete(watcher.boardId);
148
- }
149
-
150
- function pollBoardWatcher(watcher) {
151
- if (watcher.clients.size === 0) {
152
- stopBoardWatcherIfIdle(watcher);
153
- return;
154
- }
155
-
156
- let nextDataVersion;
157
- try {
158
- nextDataVersion = dataVersionForBoard(watcher.boardRow);
159
- } catch (error) {
160
- console.warn(`[orbit] Failed to read data_version for board ${watcher.boardId}: ${error.message}`);
161
- return;
162
- }
163
-
164
- if (nextDataVersion === watcher.dataVersion) return;
165
- watcher.dataVersion = nextDataVersion;
166
- drainBoardWatcherEvents(watcher);
167
- }
168
-
169
- function drainBoardWatcherEvents(watcher) {
170
- try {
171
- while (true) {
172
- const rows = eventsAfterCursor(watcher.boardRow, watcher.cursor);
173
- if (rows.length === 0) return;
174
-
175
- for (const row of rows) {
176
- const event = eventFromRow(watcher.boardRow, row);
177
- for (const client of watcher.clients) {
178
- writeSSEFrame(client.res, event);
179
- }
180
- watcher.cursor = eventCursor(row);
181
- }
182
-
183
- if (rows.length < EVENT_BATCH_LIMIT) return;
184
- }
185
- } catch (error) {
186
- console.warn(`[orbit] Failed to poll events for board ${watcher.boardId}: ${error.message}`);
187
- }
188
- }
189
-
190
- function ssePollIntervalMs() {
191
- const raw = process.env.ORBIT_SSE_POLL_MS;
192
- const value = raw === undefined ? DEFAULT_SSE_POLL_MS : Number(raw);
193
- if (!Number.isFinite(value) || value <= 0) return DEFAULT_SSE_POLL_MS;
194
- return Math.min(MAX_SSE_POLL_MS, Math.max(MIN_SSE_POLL_MS, Math.trunc(value)));
195
- }
196
-
197
- function dataVersionForBoard(boardRow) {
198
- const row = openBoardDb(boardRow).prepare("PRAGMA data_version").get();
199
- return Number(row?.data_version || 0);
200
- }
201
-
202
- function replayEventsForBoard(res, boardRow, afterId) {
203
- const db = openBoardDb(boardRow);
204
- const anchor = db.prepare("SELECT rowid AS event_rowid FROM events WHERE id = ?").get(afterId);
205
- if (!anchor) return latestEventCursor(boardRow);
206
-
207
- let cursor = eventCursor(anchor);
208
- const rows = db
209
- .prepare(
210
- `SELECT e.rowid AS event_rowid, e.id, e.type, e.ticket_id, e.actor, e.body_json, e.created_at
211
- FROM events e
212
- WHERE e.rowid > ?
213
- ORDER BY e.rowid LIMIT ${EVENT_BATCH_LIMIT}`
214
- )
215
- .all(anchor.event_rowid);
216
-
217
- for (const row of rows) {
218
- const event = eventFromRow(boardRow, row);
219
- writeSSEFrame(res, event);
220
- cursor = eventCursor(row);
221
- }
222
- return cursor;
223
- }
224
-
225
- function latestEventCursor(boardRow) {
226
- const db = openBoardDb(boardRow);
227
- const row = db
228
- .prepare("SELECT rowid AS event_rowid FROM events ORDER BY rowid DESC LIMIT 1")
229
- .get();
230
- return eventCursor(row);
231
- }
232
-
233
- function eventCursor(row) {
234
- if (!row) return null;
235
- return { event_rowid: Number(row.event_rowid) };
236
- }
237
-
238
- function compareCursors(left, right) {
239
- if (!left && !right) return 0;
240
- if (!left) return -1;
241
- if (!right) return 1;
242
- if (left.event_rowid > right.event_rowid) return 1;
243
- if (left.event_rowid < right.event_rowid) return -1;
244
- return 0;
245
- }
246
-
247
- function eventsAfterCursor(boardRow, cursor) {
248
- const db = openBoardDb(boardRow);
249
- if (!cursor) {
250
- return db
251
- .prepare(
252
- `SELECT e.rowid AS event_rowid, e.id, e.type, e.ticket_id, e.actor, e.body_json, e.created_at
253
- FROM events e
254
- ORDER BY e.rowid LIMIT ${EVENT_BATCH_LIMIT}`
255
- )
256
- .all();
257
- }
258
- return db
259
- .prepare(
260
- `SELECT e.rowid AS event_rowid, e.id, e.type, e.ticket_id, e.actor, e.body_json, e.created_at
261
- FROM events e
262
- WHERE e.rowid > ?
263
- ORDER BY e.rowid LIMIT ${EVENT_BATCH_LIMIT}`
264
- )
265
- .all(cursor.event_rowid);
266
- }
267
-
268
- function eventFromRow(boardRow, row) {
269
- return {
270
- id: row.id,
271
- type: row.type,
272
- ticket_id: row.ticket_id,
273
- board_id: boardRow.id,
274
- actor: row.actor,
275
- data: parseEventBody(row.body_json),
276
- timestamp: row.created_at
277
- };
278
- }
279
-
280
- function parseEventBody(bodyJson) {
281
- try {
282
- return JSON.parse(bodyJson || "{}");
283
- } catch {
284
- return {};
285
- }
286
- }
287
-
288
- function advanceWatcherCursorByRowid(boardId, rowid) {
289
- const watcher = boardWatchers.get(boardId);
290
- if (!watcher) return;
291
- const cursor = { event_rowid: Number(rowid) };
292
- if (cursor && compareCursors(cursor, watcher.cursor) > 0) {
293
- watcher.cursor = cursor;
294
- }
295
- }
296
-
297
- function broadcastSSE(event) {
298
- for (const client of sseClients) {
299
- if (client.boardId && event.board_id !== client.boardId) continue;
300
- writeSSEFrame(client.res, event);
301
- }
302
- }
303
-
304
- function writeSSEFrame(res, event) {
305
- if (res.writableEnded) return;
306
- // A backpressured client (closed laptop, dead tab) would otherwise make
307
- // Node buffer every event in memory forever. Cut it loose; the EventSource
308
- // client reconnects and replays missed events via Last-Event-ID.
309
- if (res.writableLength > SSE_MAX_BUFFERED_BYTES) {
310
- res.end();
311
- return;
312
- }
313
- res.write(`id: ${event.id}\nevent: board\ndata: ${JSON.stringify(event)}\n\n`);
314
- }
package/src/core/http.js DELETED
@@ -1,115 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { createReadStream } from "node:fs";
3
- import { extname, join, resolve as resolvePath } from "node:path";
4
- import { bundleCss } from "./css-bundle.js";
5
- import { PUBLIC_DIR, ROOT_DIR } from "./paths.js";
6
- import { httpError } from "./util.js";
7
-
8
- export function setCors(res) {
9
- res.setHeader("Access-Control-Allow-Origin", "*");
10
- res.setHeader("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE,OPTIONS");
11
- res.setHeader(
12
- "Access-Control-Allow-Headers",
13
- "Content-Type,Authorization,X-Orbit-Actor-Type,X-Orbit-Actor-Name,X-Orbit-Actor-Id,X-Orbit-Agent-Name,X-File-Name"
14
- );
15
- }
16
-
17
- export function sendJson(res, status, body) {
18
- setCors(res);
19
- res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
20
- res.end(JSON.stringify(body, null, 2));
21
- }
22
-
23
- export function sendEmpty(res, status) {
24
- setCors(res);
25
- res.writeHead(status);
26
- res.end();
27
- }
28
-
29
- export const MAX_JSON_BODY_BYTES = 100 * 1024 * 1024;
30
-
31
- export async function readJson(req, maxBytes = MAX_JSON_BODY_BYTES) {
32
- const buffer = await readRaw(req, maxBytes);
33
- if (buffer.length === 0) return {};
34
- const text = buffer.toString("utf8");
35
- if (!text.trim()) return {};
36
- try {
37
- return JSON.parse(text);
38
- } catch {
39
- throw httpError(400, "invalid_json");
40
- }
41
- }
42
-
43
- export async function readRaw(req, maxBytes) {
44
- const chunks = [];
45
- let size = 0;
46
- for await (const chunk of req) {
47
- size += chunk.length;
48
- if (size > maxBytes) throw httpError(413, "payload_too_large");
49
- chunks.push(chunk);
50
- }
51
- return Buffer.concat(chunks);
52
- }
53
-
54
- export function sendFile(res, filePath, mimeType) {
55
- setCors(res);
56
- res.writeHead(200, {
57
- "Content-Type": mimeType || contentType(filePath),
58
- "Cache-Control": "private, max-age=3600"
59
- });
60
- createReadStream(filePath).pipe(res);
61
- }
62
-
63
- function contentType(filePath) {
64
- const types = {
65
- ".html": "text/html; charset=utf-8",
66
- ".css": "text/css; charset=utf-8",
67
- ".js": "text/javascript; charset=utf-8",
68
- ".json": "application/json; charset=utf-8",
69
- ".svg": "image/svg+xml"
70
- };
71
- return types[extname(filePath)] || "application/octet-stream";
72
- }
73
-
74
- function shouldBundleSourceStyles(pathname, filePath) {
75
- return pathname === "/styles.css" && filePath === join(ROOT_DIR, "public", "styles.css");
76
- }
77
-
78
- export function staticAssetPathname(pathname) {
79
- return pathname === "/app" || pathname.startsWith("/app/")
80
- ? pathname.slice(4) || "/"
81
- : pathname;
82
- }
83
-
84
- export async function serveStatic(req, res, pathname) {
85
- if (req.method !== "GET" && req.method !== "HEAD") {
86
- sendEmpty(res, 405);
87
- return;
88
- }
89
-
90
- const staticPathname = staticAssetPathname(pathname);
91
- const requested = staticPathname === "/" ? "/index.html" : staticPathname;
92
- const filePath = resolvePath(PUBLIC_DIR, `.${decodeURIComponent(requested)}`);
93
- if (!filePath.startsWith(PUBLIC_DIR)) {
94
- sendEmpty(res, 403);
95
- return;
96
- }
97
-
98
- try {
99
- const data = shouldBundleSourceStyles(staticPathname, filePath)
100
- ? Buffer.from((await bundleCss({ entryPoint: filePath, outfile: filePath })).cssFile.contents)
101
- : await readFile(filePath);
102
- res.writeHead(200, {
103
- "Content-Type": contentType(filePath),
104
- "Cache-Control": "no-store"
105
- });
106
- if (req.method !== "HEAD") res.end(data);
107
- else res.end();
108
- } catch {
109
- if (pathname !== "/") {
110
- await serveStatic(req, res, "/");
111
- } else {
112
- sendEmpty(res, 404);
113
- }
114
- }
115
- }
@@ -1,198 +0,0 @@
1
- /**
2
- * migrate.js — one-time startup migration of per-board SQLite files from
3
- * in-repo .orbit/ directories into the central DATA_DIR/boards/<slug>/ store.
4
- *
5
- * Called during board.js startup, before ensureSchemaForEveryBoard, so that
6
- * by the time per-board connections are opened they are already at their
7
- * canonical central location.
8
- *
9
- * If a migration fails the registry row is NOT updated and Orbit continues
10
- * to serve from the old in-repo path. A structured warning with agent-actionable
11
- * manual instructions is printed to stderr.
12
- */
13
- import { existsSync, mkdirSync, readdirSync, rmSync, unlinkSync } from "node:fs";
14
- import { dirname, join } from "node:path";
15
- import { closeConnection, getRegistry, openConnection } from "./db.js";
16
- import { BOARDS_DIR } from "./paths.js";
17
- import { listBoards } from "./registry.js";
18
-
19
- /** Normalize to forward slashes for consistent cross-platform prefix checks. */
20
- const fwd = (p) => p.replace(/\\/g, "/");
21
-
22
- /** True when db_path already lives inside the canonical BOARDS_DIR. */
23
- function isCentral(dbPath) {
24
- return fwd(dbPath).startsWith(fwd(BOARDS_DIR) + "/");
25
- }
26
-
27
- /** Canonical central path for a given slug. */
28
- function centralPathFor(slug) {
29
- return join(BOARDS_DIR, slug, "board.db");
30
- }
31
-
32
- /** Delete a file if it exists; silently ignore errors. */
33
- function tryUnlink(p) {
34
- try { if (existsSync(p)) unlinkSync(p); } catch { /* ignore */ }
35
- }
36
-
37
- /**
38
- * Walk upward from startDir, removing each directory that is now empty.
39
- * Stops at stopDir (inclusive) or the first non-empty directory.
40
- * This cleans up empty .orbit/ trees left behind after moving the db out.
41
- */
42
- function pruneEmptyAncestors(startDir, stopDir) {
43
- const stopFwd = fwd(stopDir);
44
- let cur = fwd(startDir);
45
- while (cur.startsWith(stopFwd)) {
46
- try {
47
- if (!existsSync(cur)) {
48
- /* already gone, climb */
49
- } else if (readdirSync(cur).length > 0) {
50
- break; // non-empty — stop here
51
- } else {
52
- // rmSync({recursive:true, force:true}) works correctly for both files and
53
- // empty directories across Node versions and platforms. We've already
54
- // confirmed the directory is empty so nothing is over-deleted.
55
- rmSync(cur, { recursive: true, force: true });
56
- }
57
- } catch {
58
- break;
59
- }
60
- if (cur === stopFwd) break; // reached the limit
61
- const parent = fwd(dirname(cur));
62
- if (parent === cur) break; // filesystem root
63
- cur = parent;
64
- }
65
- }
66
-
67
- /**
68
- * For a db path inside a repo's .orbit directory, return the .orbit parent.
69
- * /repo/.orbit/board.db → /repo/.orbit
70
- * /repo/.orbit/boards/x/board.db → /repo/.orbit
71
- * Returns null if ".orbit" is not found in the path.
72
- */
73
- function orbitParentDir(dbPath) {
74
- const parts = fwd(dbPath).split("/");
75
- const idx = parts.lastIndexOf(".orbit");
76
- if (idx < 0) return null;
77
- return parts.slice(0, idx + 1).join("/");
78
- }
79
-
80
- function sqlLiteral(v) {
81
- return `'${String(v).replace(/'/g, "''")}'`;
82
- }
83
-
84
- /**
85
- * Attempt to migrate one board from in-repo storage to central storage.
86
- * Returns a result object with status: 'migrated' | 'failed' | 'missing_source'.
87
- */
88
- function migrateOneBoard(boardRow) {
89
- const { id, slug, db_path: oldPath } = boardRow;
90
-
91
- if (!existsSync(oldPath)) {
92
- // Source file is gone; ensureSchemaForEveryBoard will prune the stale row.
93
- return { status: "missing_source", board: boardRow, oldPath };
94
- }
95
-
96
- const newPath = centralPathFor(slug);
97
- mkdirSync(dirname(newPath), { recursive: true });
98
-
99
- try {
100
- // Safety: if the target path is already registered to a *different* board id,
101
- // skip rather than overwrite live data (guards against corrupted registries with
102
- // duplicate slugs — cannot happen through normal API use but is defensive).
103
- const existingOwner = getRegistry().prepare("SELECT id FROM boards WHERE db_path = ?").get(newPath);
104
- if (existingOwner && existingOwner.id !== id) {
105
- return {
106
- status: "failed",
107
- board: boardRow,
108
- oldPath,
109
- newPath,
110
- error: new Error(
111
- `central path ${newPath} is already registered to board ${existingOwner.id} — skipping to avoid data loss`
112
- )
113
- };
114
- }
115
-
116
- // Remove any stale central copy left by a previous partial migration.
117
- if (existsSync(newPath)) tryUnlink(newPath);
118
-
119
- // VACUUM INTO creates a clean, WAL-flushed copy. It captures only committed
120
- // data and produces a single-file database (no -wal/-shm at the destination).
121
- const db = openConnection(oldPath);
122
- db.exec(`VACUUM INTO ${sqlLiteral(newPath)};`);
123
- closeConnection(oldPath);
124
-
125
- // Update the registry before deleting the source so the canonical pointer is
126
- // always valid: if the process dies between these two steps the old file still
127
- // exists and the next run will redo the migration cleanly.
128
- getRegistry()
129
- .prepare("UPDATE boards SET db_path = ?, updated_at = ? WHERE id = ?")
130
- .run(newPath, new Date().toISOString(), id);
131
-
132
- // Remove source files (main db + WAL + SHM + journal).
133
- tryUnlink(oldPath);
134
- tryUnlink(oldPath + "-wal");
135
- tryUnlink(oldPath + "-shm");
136
- tryUnlink(oldPath + "-journal");
137
-
138
- // Prune now-empty .orbit/ directories from the repo tree.
139
- const orbitDir = orbitParentDir(oldPath);
140
- if (orbitDir) pruneEmptyAncestors(dirname(oldPath), orbitDir);
141
-
142
- return { status: "migrated", board: boardRow, oldPath, newPath };
143
- } catch (error) {
144
- // Ensure no lingering open connection or partial central file on failure.
145
- try { closeConnection(oldPath); } catch { /* ignore */ }
146
- tryUnlink(newPath);
147
- return { status: "failed", board: boardRow, oldPath, newPath, error };
148
- }
149
- }
150
-
151
- /**
152
- * Migrate all boards whose db_path lives outside DATA_DIR into central storage.
153
- * Called at server/MCP startup before any per-board connections are opened.
154
- * Prints a concise summary for migrated boards; prints agent-actionable
155
- * instructions for any failures so a human or AI can complete the migration.
156
- */
157
- export function migrateInRepoBoards() {
158
- const pending = listBoards().filter((row) => !isCentral(row.db_path));
159
- if (pending.length === 0) return;
160
-
161
- const results = pending.map(migrateOneBoard);
162
- const migrated = results.filter((r) => r.status === "migrated");
163
- const failed = results.filter((r) => r.status === "failed");
164
-
165
- if (migrated.length > 0) {
166
- const n = migrated.length;
167
- console.log(`[orbit] Migrated ${n} board${n > 1 ? "s" : ""} to central storage (${BOARDS_DIR}):`);
168
- for (const r of migrated) {
169
- console.log(` ✓ ${r.board.slug}`);
170
- console.log(` from: ${r.oldPath}`);
171
- console.log(` to: ${r.newPath}`);
172
- }
173
- }
174
-
175
- if (failed.length > 0) {
176
- const sep = " │";
177
- const n = failed.length;
178
- console.error(`\n[orbit] ⚠ Could not auto-migrate ${n} board${n > 1 ? "s" : ""} to central storage.`);
179
- console.error(`${sep} These boards continue to work from their current paths.`);
180
- console.error(`${sep} To complete the migration manually, run the commands below and`);
181
- console.error(`${sep} restart orbit run:\n`);
182
- for (const r of failed) {
183
- const slash = process.platform === "win32" ? "\\" : "/";
184
- const destDir = dirname(r.newPath).replace(/\//g, slash);
185
- const src = r.oldPath.replace(/\//g, slash);
186
- const dest = r.newPath.replace(/\//g, slash);
187
- console.error(`${sep} Board: ${r.board.slug} (error: ${r.error?.message ?? r.error})`);
188
- if (process.platform === "win32") {
189
- console.error(`${sep} md "${destDir}"`);
190
- console.error(`${sep} copy "${src}" "${dest}"`);
191
- } else {
192
- console.error(`${sep} mkdir -p '${destDir}'`);
193
- console.error(`${sep} cp '${src}' '${dest}'`);
194
- }
195
- console.error(sep);
196
- }
197
- }
198
- }
package/src/core/paths.js DELETED
@@ -1,55 +0,0 @@
1
- import { existsSync, mkdirSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join, resolve } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
7
-
8
- export const ROOT_DIR = resolve(__dirname, "..", "..");
9
-
10
- const _distFull = join(ROOT_DIR, "dist", "full");
11
- const _publicDir = join(ROOT_DIR, "public");
12
-
13
- /** Where static assets (HTML/JS/CSS) are served from.
14
- *
15
- * public/ — editable source; `pnpm dev` bundles @starlove/ui imports on the fly.
16
- * dist/full — prebuilt, hashed bundles from `pnpm build`; what Docker/npm ship.
17
- *
18
- * Auto (default): public/ when it exists, else dist/full. Repo dev therefore
19
- * always sees live CSS/JS edits. Docker and other dist-only installs fall back
20
- * to the bundle without needing an env var.
21
- *
22
- * Override: ORBIT_USE_DIST=1 force dist/full, ORBIT_USE_DIST=0 force public/. */
23
- function resolvePublicDir() {
24
- const distExists = existsSync(_distFull);
25
- const publicExists = existsSync(_publicDir);
26
- const pref = process.env.ORBIT_USE_DIST;
27
-
28
- if (pref === "1" && distExists) return _distFull;
29
- if (pref === "0" && publicExists) return _publicDir;
30
- if (publicExists) return _publicDir;
31
- if (distExists) return _distFull;
32
- return _publicDir;
33
- }
34
-
35
- export const PUBLIC_DIR = resolvePublicDir();
36
- export const MCP_SERVER_PATH = join(ROOT_DIR, "src", "mcp-server.js");
37
- /** Repo root for default board placement; cwd unless PROJECT_ROOT is set. */
38
- export const PROJECT_ROOT = process.env.PROJECT_ROOT ? resolve(process.env.PROJECT_ROOT) : resolve(process.cwd());
39
- // Store registry + exports in ~/.orbit so global installs don't write into the package dir.
40
- export const DATA_DIR = process.env.DATA_DIR || join(homedir(), ".orbit");
41
- /** Central registry of all boards (metadata + paths to per-board SQLite files). */
42
- export const REGISTRY_DB_PATH = join(DATA_DIR, "registry.db");
43
- /** Central directory where every board's SQLite file lives: DATA_DIR/boards/<slug>/board.db */
44
- export const BOARDS_DIR = join(DATA_DIR, "boards");
45
- export const EXPORT_DIR = join(DATA_DIR, "exports");
46
- export const BACKUP_DIR = join(DATA_DIR, "backups");
47
- /** Central directory for dispatch run artifacts: DATA_DIR/dispatch-runs/<board-slug>/<run-id>/ */
48
- export const DISPATCH_RUNS_DIR = join(DATA_DIR, "dispatch-runs");
49
- export const PORT = Number(process.env.PORT || 13701);
50
-
51
- mkdirSync(DATA_DIR, { recursive: true });
52
- mkdirSync(BOARDS_DIR, { recursive: true });
53
- mkdirSync(EXPORT_DIR, { recursive: true });
54
- mkdirSync(BACKUP_DIR, { recursive: true });
55
- mkdirSync(DISPATCH_RUNS_DIR, { recursive: true });