@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
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Orbit's errors. Each carries a code, the same codes Orbit's answers have
3
+ * always used (`invalid_state`, `board_not_found`...), and a kind that says
4
+ * what went wrong in words any transport understands. The core and the storage
5
+ * adapters throw them; each transport maps the kind to its own form: an HTTP
6
+ * status (`HTTP_STATUS` below), an MCP tool error, a CLI exit code.
7
+ *
8
+ * `message` is what people and agents read. It is the code itself unless a
9
+ * clearer sentence was given, which is what Orbit's answers and the command
10
+ * language ("Orbit refused: invalid state.") already show.
11
+ */
12
+ /** Every code Orbit raises, with its kind. */
13
+ export const ERROR_CODES = {
14
+ // Boards
15
+ board_not_found: "not_found",
16
+ board_id_required: "invalid",
17
+ board_id_mismatch: "invalid",
18
+ board_slug_taken: "conflict",
19
+ board_slug_confirmation_required: "invalid",
20
+ board_has_no_states: "invalid",
21
+ board_files_busy: "conflict",
22
+ board_file_missing: "conflict",
23
+ unsafe_board_db_path: "conflict",
24
+ missing_name: "invalid",
25
+ // Lanes
26
+ state_not_found: "not_found",
27
+ invalid_state: "invalid",
28
+ invalid_state_order: "invalid",
29
+ invalid_state_role: "invalid",
30
+ state_has_tickets: "conflict",
31
+ cannot_delete_last_state: "conflict",
32
+ cannot_rename_ai_ready_lane: "conflict",
33
+ ai_ready_lane_must_be_named_ai_ready: "conflict",
34
+ missing_in_progress_state: "invalid",
35
+ missing_review_state: "invalid",
36
+ missing_next_state: "invalid",
37
+ // Cards
38
+ ticket_not_found: "not_found",
39
+ missing_title: "invalid",
40
+ invalid_ticket_type: "invalid",
41
+ invalid_number: "invalid",
42
+ invalid_nebula_url: "invalid",
43
+ ticket_id_or_number_or_title_required: "invalid",
44
+ missing_ticket_id: "invalid",
45
+ ticket_not_archived: "conflict",
46
+ invalid_parent_ticket: "invalid",
47
+ epic_cannot_have_parent: "invalid",
48
+ ticket_cannot_parent_itself: "invalid",
49
+ ticket_parent_cycle: "invalid",
50
+ missing_label: "invalid",
51
+ // Comments
52
+ comment_not_found: "not_found",
53
+ missing_body: "invalid",
54
+ omitted_for_ai_required: "invalid",
55
+ // Relations
56
+ relation_not_found: "not_found",
57
+ relation_exists: "conflict",
58
+ relation_self: "invalid",
59
+ relation_redundant_with_parent: "invalid",
60
+ hierarchy_uses_parent_ticket_id: "invalid",
61
+ blocker_target_already_done: "invalid",
62
+ blocker_source_already_done: "invalid",
63
+ invalid_relation_type: "invalid",
64
+ missing_source_ticket_id: "invalid",
65
+ missing_target_ticket_id: "invalid",
66
+ // Journal
67
+ board_entry_not_found: "not_found",
68
+ invalid_project_entry_type: "invalid",
69
+ missing_entry_title: "invalid",
70
+ entry_not_struck: "conflict",
71
+ // Agents
72
+ forbidden: "forbidden",
73
+ missing_message: "invalid",
74
+ missing_summary: "invalid",
75
+ invalid_since: "invalid",
76
+ // Review verdicts
77
+ review_verdict_not_found: "not_found",
78
+ missing_verdict: "invalid",
79
+ invalid_review_verdict: "invalid",
80
+ invalid_blocking_findings: "invalid",
81
+ invalid_optional_findings: "invalid",
82
+ invalid_evidence_commands: "invalid",
83
+ invalid_supersedes_prior_review_id: "invalid",
84
+ superseded_review_ticket_mismatch: "invalid",
85
+ // Images
86
+ attachment_not_found: "not_found",
87
+ attachment_file_missing: "not_found",
88
+ empty_attachment: "invalid",
89
+ invalid_attachment_path: "invalid",
90
+ attachment_too_large: "too_large",
91
+ unsupported_attachment_type: "unsupported_type",
92
+ attachment_type_mismatch: "unsupported_type",
93
+ // Markdown artifacts
94
+ artifact_not_found: "not_found",
95
+ artifact_path_required: "invalid",
96
+ artifact_path_outside_allowed_roots: "invalid",
97
+ artifact_root_unavailable: "invalid",
98
+ artifact_not_file: "invalid",
99
+ artifact_too_large: "too_large",
100
+ unsupported_artifact_type: "unsupported_type",
101
+ // Import and export
102
+ invalid_import_snapshot: "invalid",
103
+ snapshot_missing_board: "invalid",
104
+ import_requires_empty_board_or_replace_existing: "conflict",
105
+ import_would_delete_hidden_comments: "conflict",
106
+ // Requests (raised by the HTTP transport)
107
+ not_found: "not_found",
108
+ method_not_allowed: "method_not_allowed",
109
+ invalid_json: "invalid",
110
+ invalid_body: "invalid",
111
+ payload_too_large: "too_large",
112
+ cross_site_request: "forbidden",
113
+ folder_picker_unavailable: "not_supported",
114
+ // Storage (raised by adapters; see docs/ARCHITECTURE.md)
115
+ duplicate_id: "conflict",
116
+ ticket_number_taken: "conflict",
117
+ state_name_taken: "conflict",
118
+ label_name_taken: "conflict",
119
+ invalid_reference: "invalid",
120
+ invalid_blob_path: "invalid",
121
+ blob_exists: "conflict",
122
+ write_closed: "internal",
123
+ storage_busy: "unavailable",
124
+ storage_full: "unavailable",
125
+ storage_failed: "internal",
126
+ // Data folders (raised when a store opens; see docs/ARCHITECTURE.md, "Adapter notes")
127
+ data_needs_migration: "unavailable",
128
+ data_from_newer_orbit: "unavailable",
129
+ // `orbit migrate`: a source it can't read, or a folder it won't write into
130
+ migration_source_unreadable: "invalid",
131
+ migration_target_in_use: "conflict"
132
+ };
133
+ /** The HTTP status each kind answers with: the statuses Orbit's API has always used for these codes. */
134
+ export const HTTP_STATUS = {
135
+ invalid: 400,
136
+ unauthorized: 401,
137
+ forbidden: 403,
138
+ not_found: 404,
139
+ method_not_allowed: 405,
140
+ conflict: 409,
141
+ too_large: 413,
142
+ unsupported_type: 415,
143
+ not_supported: 501,
144
+ unavailable: 503,
145
+ internal: 500
146
+ };
147
+ export class OrbitError extends Error {
148
+ code;
149
+ kind;
150
+ constructor(code, options = {}) {
151
+ super(options.message ?? code, options.cause === undefined ? undefined : { cause: options.cause });
152
+ this.name = "OrbitError";
153
+ this.code = code;
154
+ this.kind = ERROR_CODES[code];
155
+ }
156
+ }
157
+ /** True for an OrbitError, from this copy of Orbit or another (a host may bundle its own). */
158
+ export function isOrbitError(value) {
159
+ return value instanceof OrbitError || (value instanceof Error && value.name === "OrbitError" && typeof value.code === "string" && typeof value.kind === "string");
160
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Images on cards, and the markdown files a card links to (a plan, a report).
3
+ * Their bytes live in the BlobStore: images under `attachments/`, markdown
4
+ * under `artifacts/`. Bytes are written before the record that names them and
5
+ * removed after the record is gone, so a failure leaves an unused file, never
6
+ * a card whose image has no bytes.
7
+ */
8
+ import type { BoardCtx } from "./context.ts";
9
+ import type { Attachment, Id } from "./model.ts";
10
+ export declare const MAX_IMAGE_BYTES: number;
11
+ export declare const MAX_MARKDOWN_BYTES: number;
12
+ export { DOWNLOAD_TYPE, isShownImage, unsupportedTypeMessage } from "./image-types.ts";
13
+ /** The image type the bytes themselves say, from their first bytes, or null when they look like none Orbit keeps. */
14
+ export declare function sniffImage(bytes: Uint8Array): string | null;
15
+ /**
16
+ * The type an imported or migrated image is stored as: the type its bytes
17
+ * say when that is one Orbit shows, whatever the file claimed; otherwise (an
18
+ * SVG, bytes no image type matches, or bytes that aren't what they claim)
19
+ * `DOWNLOAD_TYPE`, so the file is kept but only ever offered as a download.
20
+ * Without bytes, the type it claims, when Orbit shows that type.
21
+ */
22
+ export declare function storedImageType(declared: unknown, bytes: Uint8Array | null): string;
23
+ export interface AttachmentView extends Attachment {
24
+ missing: boolean;
25
+ /** True when Orbit won't show it as an image (an SVG, or a type it can't vouch for): the app offers it as a file to download. */
26
+ download_only: boolean;
27
+ content_url: string | null;
28
+ filesystem_size_bytes: number | null;
29
+ }
30
+ export declare function listAttachments(c: BoardCtx, ticketId: Id): Promise<{
31
+ attachments: AttachmentView[];
32
+ }>;
33
+ /** An image's bytes and type, and its name. A type Orbit won't show comes back as `DOWNLOAD_TYPE`, with `download_only` set. */
34
+ export declare function attachmentContent(c: BoardCtx, ticketId: Id, attachmentId: Id): Promise<{
35
+ bytes: Uint8Array;
36
+ mime_type: string;
37
+ download_only: boolean;
38
+ original_name: string;
39
+ }>;
40
+ /** Adds an image to a card. Its bytes must be the type it says: a PNG named image/jpeg is refused. */
41
+ export declare function createAttachment(c: BoardCtx, ticketId: Id, input: {
42
+ bytes: Uint8Array;
43
+ mime_type?: unknown;
44
+ original_name?: unknown;
45
+ }): Promise<AttachmentView>;
46
+ export declare function deleteAttachment(c: BoardCtx, ticketId: Id, attachmentId: Id): Promise<{
47
+ ok: true;
48
+ deleted_id: Id;
49
+ }>;
50
+ /** An image as an export carries it: its bytes as base64 when asked for and present. */
51
+ export declare function exportedAttachment(c: BoardCtx, attachment: Attachment, includeBytes: boolean): Promise<AttachmentView & {
52
+ data_base64: string | null;
53
+ included: boolean;
54
+ }>;
55
+ /**
56
+ * An image an import brings, its bytes stored when it carries them; the
57
+ * record to write. Its bytes are sniffed as an upload's are, and stored under
58
+ * `storedImageType`: an SVG or a file that isn't what it says is kept as a
59
+ * download only.
60
+ */
61
+ export declare function importedAttachment(c: BoardCtx, row: Record<string, unknown>, ticketId: Id): Promise<Attachment>;
62
+ export interface StoredArtifact {
63
+ ticket_id: Id;
64
+ /** Relative to the board's artifacts. */
65
+ path: string;
66
+ /** Where the file is on this machine, for the person's own app; never for an agent. */
67
+ absolute_path?: string;
68
+ size_bytes: number;
69
+ }
70
+ /** Saves markdown for a card (a plan, a report) beside the board: `tickets/<card id>/<name>.md`. */
71
+ export declare function writeArtifact(c: BoardCtx, ticketId: Id, input: {
72
+ filename?: unknown;
73
+ path?: unknown;
74
+ content?: unknown;
75
+ }): Promise<StoredArtifact>;
76
+ /** Reads a markdown file in the board's artifacts. */
77
+ export declare function readArtifact(c: BoardCtx, ticketId: Id, requested: unknown): Promise<StoredArtifact & {
78
+ content: string;
79
+ }>;
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Images on cards, and the markdown files a card links to (a plan, a report).
3
+ * Their bytes live in the BlobStore: images under `attachments/`, markdown
4
+ * under `artifacts/`. Bytes are written before the record that names them and
5
+ * removed after the record is gone, so a failure leaves an unused file, never
6
+ * a card whose image has no bytes.
7
+ */
8
+ import { ARTIFACTS } from "./card-paths.js";
9
+ import { DOWNLOAD_TYPE, IMAGE_EXTENSIONS, isShownImage, unsupportedTypeMessage } from "./image-types.js";
10
+ import { fromBase64, fromUtf8, sha256Hex, toBase64, utf8 } from "./bytes.js";
11
+ import { isAgent } from "./context.js";
12
+ import { OrbitError } from "./errors.js";
13
+ import { record, touchTicket } from "./history.js";
14
+ import { ticketOn } from "./tickets.js";
15
+ import { asText } from "./text.js";
16
+ export const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
17
+ export const MAX_MARKDOWN_BYTES = 512 * 1024;
18
+ export { DOWNLOAD_TYPE, isShownImage, unsupportedTypeMessage } from "./image-types.js";
19
+ const startsWith = (bytes, prefix, at = 0) => prefix.every((byte, i) => bytes[at + i] === byte);
20
+ const ascii = (text) => Array.from(text, (ch) => ch.charCodeAt(0));
21
+ /** The image type the bytes themselves say, from their first bytes, or null when they look like none Orbit keeps. */
22
+ export function sniffImage(bytes) {
23
+ if (startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
24
+ return "image/png";
25
+ if (startsWith(bytes, [0xff, 0xd8, 0xff]))
26
+ return "image/jpeg";
27
+ if (startsWith(bytes, ascii("GIF87a")) || startsWith(bytes, ascii("GIF89a")))
28
+ return "image/gif";
29
+ if (startsWith(bytes, ascii("RIFF")) && startsWith(bytes, ascii("WEBP"), 8))
30
+ return "image/webp";
31
+ if (startsWith(bytes, ascii("BM")))
32
+ return "image/bmp";
33
+ if (startsWith(bytes, ascii("ftyp"), 4) && (startsWith(bytes, ascii("avif"), 8) || startsWith(bytes, ascii("avis"), 8)))
34
+ return "image/avif";
35
+ const head = fromUtf8(bytes.subarray(0, 1024)).replace(/^\uFEFF/, "").trimStart();
36
+ if (head.startsWith("<") && /<svg[\s>]/i.test(fromUtf8(bytes.subarray(0, 64 * 1024))))
37
+ return "image/svg+xml";
38
+ return null;
39
+ }
40
+ function imageMime(value) {
41
+ const mime = (asText(value ?? "").split(";")[0] ?? "").trim().toLowerCase();
42
+ if (!IMAGE_EXTENSIONS.has(mime))
43
+ throw new OrbitError("unsupported_attachment_type", { message: unsupportedTypeMessage(mime) });
44
+ return mime;
45
+ }
46
+ function safeOriginalName(value) {
47
+ const name = asText(value || "image").replace(/[\0\r\n]/g, "").trim();
48
+ return name.slice(0, 240) || "image";
49
+ }
50
+ function extensionOf(name) {
51
+ const base = name.split(/[\\/]/).pop() ?? "";
52
+ const dot = base.lastIndexOf(".");
53
+ return dot > 0 ? base.slice(dot).toLowerCase() : "";
54
+ }
55
+ function storedPathFor(ticketId, attachmentId, mime, originalName) {
56
+ const fromName = extensionOf(originalName);
57
+ const shown = IMAGE_EXTENSIONS.get(mime);
58
+ const ext = shown ? (fromName === shown || (mime === "image/jpeg" && fromName === ".jpeg") ? fromName : shown) : ".bin";
59
+ return `tickets/${ticketId}/${attachmentId}${ext}`;
60
+ }
61
+ /**
62
+ * The type an imported or migrated image is stored as: the type its bytes
63
+ * say when that is one Orbit shows, whatever the file claimed; otherwise (an
64
+ * SVG, bytes no image type matches, or bytes that aren't what they claim)
65
+ * `DOWNLOAD_TYPE`, so the file is kept but only ever offered as a download.
66
+ * Without bytes, the type it claims, when Orbit shows that type.
67
+ */
68
+ export function storedImageType(declared, bytes) {
69
+ if (bytes) {
70
+ const sniffed = sniffImage(bytes);
71
+ return sniffed && isShownImage(sniffed) ? sniffed : DOWNLOAD_TYPE;
72
+ }
73
+ const mime = (asText(declared ?? "").split(";")[0] ?? "").trim().toLowerCase();
74
+ return isShownImage(mime) ? mime : DOWNLOAD_TYPE;
75
+ }
76
+ const blobPath = (attachment) => `attachments/${attachment.stored_path}`;
77
+ async function viewOf(c, attachment) {
78
+ const size = await c.blobs.size(c.board.id, blobPath(attachment)).catch(() => null);
79
+ const missing = size === null;
80
+ return {
81
+ ...attachment,
82
+ missing,
83
+ download_only: !isShownImage(attachment.mime_type),
84
+ content_url: missing ? null : `/api/tickets/${encodeURIComponent(attachment.ticket_id)}/attachments/${encodeURIComponent(attachment.id)}/content`,
85
+ filesystem_size_bytes: size
86
+ };
87
+ }
88
+ export async function listAttachments(c, ticketId) {
89
+ const attachments = await c.data.read(async (r) => {
90
+ await ticketOn(r, ticketId);
91
+ return r.attachments.list(ticketId);
92
+ });
93
+ return { attachments: await Promise.all(attachments.map((attachment) => viewOf(c, attachment))) };
94
+ }
95
+ /** An image's bytes and type, and its name. A type Orbit won't show comes back as `DOWNLOAD_TYPE`, with `download_only` set. */
96
+ export async function attachmentContent(c, ticketId, attachmentId) {
97
+ const attachment = await c.data.read(async (r) => {
98
+ await ticketOn(r, ticketId);
99
+ return r.attachments.get(attachmentId);
100
+ });
101
+ if (attachment?.ticket_id !== ticketId)
102
+ throw new OrbitError("attachment_not_found");
103
+ const bytes = await c.blobs.get(c.board.id, blobPath(attachment));
104
+ if (!bytes)
105
+ throw new OrbitError("attachment_file_missing");
106
+ const shown = isShownImage(attachment.mime_type);
107
+ return { bytes, mime_type: shown ? attachment.mime_type : DOWNLOAD_TYPE, download_only: !shown, original_name: attachment.original_name };
108
+ }
109
+ /** Adds an image to a card. Its bytes must be the type it says: a PNG named image/jpeg is refused. */
110
+ export async function createAttachment(c, ticketId, input) {
111
+ await c.data.read((r) => ticketOn(r, ticketId));
112
+ const mime = imageMime(input.mime_type);
113
+ const bytes = input.bytes;
114
+ if (bytes.length === 0)
115
+ throw new OrbitError("empty_attachment");
116
+ if (bytes.length > MAX_IMAGE_BYTES)
117
+ throw new OrbitError("attachment_too_large");
118
+ if (sniffImage(bytes) !== mime)
119
+ throw new OrbitError("attachment_type_mismatch", { message: `These bytes aren't ${mime}.` });
120
+ const originalName = safeOriginalName(input.original_name);
121
+ const id = c.ids.next();
122
+ const storedPath = storedPathFor(ticketId, id, mime, originalName);
123
+ const sha256 = await sha256Hex(bytes);
124
+ await c.blobs.put(c.board.id, `attachments/${storedPath}`, bytes, { exclusive: true });
125
+ try {
126
+ const attachment = await c.data.write(async (w) => {
127
+ const ticket = await ticketOn(w, ticketId);
128
+ const at = c.clock.now();
129
+ const made = { id, ticket_id: ticketId, board_id: c.board.id, original_name: originalName, stored_path: storedPath, mime_type: mime, size_bytes: bytes.length, sha256, created_by: c.actor.name, created_at: at };
130
+ await w.attachments.insert(made);
131
+ await w.tickets.update(ticketId, { updated_at: at });
132
+ await record(w, c, "attachment_created", ticketId, { attachment_id: id, original_name: originalName, mime_type: mime, size_bytes: bytes.length, sha256 });
133
+ await touchTicket(w, ticket.parent_ticket_id, at);
134
+ return made;
135
+ });
136
+ c.changed(c.board.id);
137
+ return await viewOf(c, attachment);
138
+ }
139
+ catch (error) {
140
+ await c.blobs.remove(c.board.id, `attachments/${storedPath}`).catch(() => undefined);
141
+ throw error;
142
+ }
143
+ }
144
+ export async function deleteAttachment(c, ticketId, attachmentId) {
145
+ const removed = await c.data.write(async (w) => {
146
+ const ticket = await ticketOn(w, ticketId);
147
+ const attachment = await w.attachments.get(attachmentId);
148
+ if (attachment?.ticket_id !== ticketId)
149
+ throw new OrbitError("attachment_not_found");
150
+ await w.attachments.remove(attachmentId);
151
+ const at = c.clock.now();
152
+ await w.tickets.update(ticketId, { updated_at: at });
153
+ await record(w, c, "attachment_deleted", ticketId, { attachment_id: attachmentId, original_name: attachment.original_name });
154
+ await touchTicket(w, ticket.parent_ticket_id, at);
155
+ return attachment;
156
+ });
157
+ await c.blobs.remove(c.board.id, blobPath(removed)).catch(() => undefined);
158
+ c.changed(c.board.id);
159
+ return { ok: true, deleted_id: attachmentId };
160
+ }
161
+ /** An image as an export carries it: its bytes as base64 when asked for and present. */
162
+ export async function exportedAttachment(c, attachment, includeBytes) {
163
+ const view = await viewOf(c, attachment);
164
+ if (!includeBytes || view.missing)
165
+ return { ...view, data_base64: null, included: false };
166
+ const bytes = await c.blobs.get(c.board.id, blobPath(attachment));
167
+ if (!bytes)
168
+ return { ...view, missing: true, content_url: null, filesystem_size_bytes: null, data_base64: null, included: false };
169
+ return { ...view, data_base64: toBase64(bytes), included: true, missing: false };
170
+ }
171
+ /**
172
+ * An image an import brings, its bytes stored when it carries them; the
173
+ * record to write. Its bytes are sniffed as an upload's are, and stored under
174
+ * `storedImageType`: an SVG or a file that isn't what it says is kept as a
175
+ * download only.
176
+ */
177
+ export async function importedAttachment(c, row, ticketId) {
178
+ const id = typeof row.id === "string" && row.id ? row.id : c.ids.next();
179
+ const bytes = typeof row.data_base64 === "string" && row.data_base64 ? fromBase64(row.data_base64) : null;
180
+ if (bytes && bytes.length > MAX_IMAGE_BYTES)
181
+ throw new OrbitError("attachment_too_large");
182
+ const mime = storedImageType(row.mime_type || "image/png", bytes);
183
+ const originalName = safeOriginalName(row.original_name);
184
+ const storedPath = storedPathFor(ticketId, id, mime, originalName);
185
+ let size = Number(row.size_bytes || 0);
186
+ let sha = asText(row.sha256 || "");
187
+ if (bytes) {
188
+ size = bytes.length;
189
+ sha = await sha256Hex(bytes);
190
+ await c.blobs.put(c.board.id, `attachments/${storedPath}`, bytes);
191
+ }
192
+ return {
193
+ id,
194
+ ticket_id: ticketId,
195
+ board_id: c.board.id,
196
+ original_name: originalName,
197
+ stored_path: storedPath,
198
+ mime_type: mime,
199
+ size_bytes: size,
200
+ sha256: sha,
201
+ created_by: typeof row.created_by === "string" && row.created_by ? row.created_by : "import",
202
+ created_at: typeof row.created_at === "string" && row.created_at ? row.created_at : c.clock.now()
203
+ };
204
+ }
205
+ // Markdown artifacts
206
+ const MARKDOWN_EXTENSIONS = new Set([".md", ".markdown", ".txt"]);
207
+ function cleanArtifactPath(value) {
208
+ const text = asText(value ?? "").trim();
209
+ if (!text || text.includes("\0"))
210
+ throw new OrbitError("artifact_path_required");
211
+ return text.replace(/\\/g, "/");
212
+ }
213
+ function markdownName(value) {
214
+ const raw = cleanArtifactPath(value || "artifact.md");
215
+ const name = (raw.split("/").pop() ?? "").replace(/[^A-Za-z0-9._-]+/g, "-") || "artifact.md";
216
+ if (!MARKDOWN_EXTENSIONS.has(extensionOf(name)))
217
+ throw new OrbitError("unsupported_artifact_type");
218
+ return name;
219
+ }
220
+ /** Saves markdown for a card (a plan, a report) beside the board: `tickets/<card id>/<name>.md`. */
221
+ export async function writeArtifact(c, ticketId, input) {
222
+ const ticket = await c.data.read((r) => ticketOn(r, ticketId));
223
+ const bytes = utf8(asText(input.content ?? ""));
224
+ if (bytes.length > MAX_MARKDOWN_BYTES)
225
+ throw new OrbitError("artifact_too_large");
226
+ const path = `tickets/${ticket.id}/${markdownName(input.filename || input.path || "artifact.md")}`;
227
+ await c.blobs.put(c.board.id, ARTIFACTS + path, bytes);
228
+ return withLocation(c, { ticket_id: ticket.id, path, size_bytes: bytes.length });
229
+ }
230
+ function withLocation(c, answer) {
231
+ if (isAgent(c.actor) || !c.blobs.location)
232
+ return answer;
233
+ return { ...answer, absolute_path: c.blobs.location(c.board.id, ARTIFACTS + answer.path) };
234
+ }
235
+ /** The path inside the board's artifacts that `requested` names: relative to them, or this machine's full path to one. */
236
+ function artifactPath(c, requested) {
237
+ let path = requested;
238
+ const root = c.blobs.location?.(c.board.id, ARTIFACTS.slice(0, -1));
239
+ if (root && (path === root || path.startsWith(`${root}/`)))
240
+ path = path.slice(root.length + 1);
241
+ else if (/^([a-z]:)?\//i.test(path))
242
+ throw new OrbitError("artifact_path_outside_allowed_roots");
243
+ path = path.replace(/^(\.\/)+/, "");
244
+ if (path.startsWith(ARTIFACTS))
245
+ path = path.slice(ARTIFACTS.length);
246
+ if (!path || path.split("/").some((segment) => segment === ".." || segment === "." || segment === ""))
247
+ throw new OrbitError("artifact_path_outside_allowed_roots");
248
+ return path;
249
+ }
250
+ /** Reads a markdown file in the board's artifacts. */
251
+ export async function readArtifact(c, ticketId, requested) {
252
+ const ticket = await c.data.read((r) => ticketOn(r, ticketId));
253
+ const path = artifactPath(c, cleanArtifactPath(requested));
254
+ if (!MARKDOWN_EXTENSIONS.has(extensionOf(path)))
255
+ throw new OrbitError("unsupported_artifact_type");
256
+ const bytes = await c.blobs.get(c.board.id, ARTIFACTS + path);
257
+ if (!bytes)
258
+ throw new OrbitError("artifact_not_found");
259
+ if (bytes.length > MAX_MARKDOWN_BYTES)
260
+ throw new OrbitError("artifact_too_large");
261
+ return { ...withLocation(c, { ticket_id: ticket.id, path, size_bytes: bytes.length }), content: fromUtf8(bytes) };
262
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * History and search, as every write keeps them: each change appends an event
3
+ * (the live stream, an agent's `changes` and the quick menu's sessions read
4
+ * them), and each change to a card's text replaces its search document.
5
+ */
6
+ import type { Ctx } from "./context.ts";
7
+ import type { Comment, Id, OrbitEvent, Ticket } from "./model.ts";
8
+ import type { BoardWriter, SearchDocument } from "../store/types.ts";
9
+ /** Appends an event by the acting actor, now. */
10
+ export declare function record(w: BoardWriter, c: Ctx, type: string, ticketId: Id | null, body: Record<string, unknown>): Promise<OrbitEvent>;
11
+ /**
12
+ * A card's search document: its text, and the comments agents may read. A
13
+ * comment hidden from agents is never given to the index, so no search can
14
+ * find it.
15
+ */
16
+ export declare function searchDocument(ticket: Ticket, comments: readonly Comment[]): SearchDocument;
17
+ /** Rebuilds one card's search document from what the write sees. */
18
+ export declare function reindex(w: BoardWriter, ticketId: Id): Promise<void>;
19
+ /** Moves a card's `updated_at` without an event: an epic follows its sub-cards' changes, so it sorts near them. */
20
+ export declare function touchTicket(w: BoardWriter, ticketId: Id | null | undefined, at: string): Promise<void>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * History and search, as every write keeps them: each change appends an event
3
+ * (the live stream, an agent's `changes` and the quick menu's sessions read
4
+ * them), and each change to a card's text replaces its search document.
5
+ */
6
+ import { isOrbitError } from "./errors.js";
7
+ /** Appends an event by the acting actor, now. */
8
+ export async function record(w, c, type, ticketId, body) {
9
+ const event = {
10
+ id: c.ids.next(),
11
+ ticket_id: ticketId,
12
+ actor: c.actor.name,
13
+ type,
14
+ body_json: JSON.stringify(body),
15
+ created_at: c.clock.now()
16
+ };
17
+ await w.events.append(event);
18
+ return event;
19
+ }
20
+ /**
21
+ * A card's search document: its text, and the comments agents may read. A
22
+ * comment hidden from agents is never given to the index, so no search can
23
+ * find it.
24
+ */
25
+ export function searchDocument(ticket, comments) {
26
+ return {
27
+ title: ticket.title,
28
+ description: ticket.description,
29
+ type: ticket.type,
30
+ details: [ticket.ai_plan, ticket.implementation_summary, ticket.implementation_updates].filter(Boolean).join("\n"),
31
+ comments: comments.filter((comment) => !comment.omitted_for_ai).map((comment) => comment.body).join("\n")
32
+ };
33
+ }
34
+ /** Rebuilds one card's search document from what the write sees. */
35
+ export async function reindex(w, ticketId) {
36
+ const ticket = await w.tickets.get(ticketId);
37
+ if (!ticket)
38
+ return;
39
+ await w.search.index(ticketId, searchDocument(ticket, await w.comments.list(ticketId)));
40
+ }
41
+ /** Moves a card's `updated_at` without an event: an epic follows its sub-cards' changes, so it sorts near them. */
42
+ export async function touchTicket(w, ticketId, at) {
43
+ if (!ticketId)
44
+ return;
45
+ try {
46
+ await w.tickets.update(ticketId, { updated_at: at });
47
+ }
48
+ catch (error) {
49
+ // An epic deleted since keeps its id on its sub-cards; there's nothing to move.
50
+ if (!(isOrbitError(error) && error.code === "ticket_not_found"))
51
+ throw error;
52
+ }
53
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Which attachments Orbit shows as images, apart from the rest of the core
3
+ * so a client (the HTTP client, an object URL) can ask without loading it.
4
+ */
5
+ /**
6
+ * The image types Orbit shows: raster formats a browser draws without running
7
+ * anything. An SVG can carry script, so Orbit neither takes a new one nor
8
+ * shows an old one; see `storedImageType` in `files.ts`.
9
+ */
10
+ export declare const IMAGE_EXTENSIONS: Map<string, string>;
11
+ /** What an upload of another type is told, naming the type it sent (`mime`, already lowercased, without parameters). */
12
+ export declare function unsupportedTypeMessage(mime: string): string;
13
+ /** What a file Orbit won't show is stored and served as: bytes to download, never a page. */
14
+ export declare const DOWNLOAD_TYPE = "application/octet-stream";
15
+ /** True for a type Orbit shows as an image; any other attachment is a file to download. */
16
+ export declare function isShownImage(mime: string): boolean;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Which attachments Orbit shows as images, apart from the rest of the core
3
+ * so a client (the HTTP client, an object URL) can ask without loading it.
4
+ */
5
+ /**
6
+ * The image types Orbit shows: raster formats a browser draws without running
7
+ * anything. An SVG can carry script, so Orbit neither takes a new one nor
8
+ * shows an old one; see `storedImageType` in `files.ts`.
9
+ */
10
+ export const IMAGE_EXTENSIONS = new Map([
11
+ ["image/png", ".png"],
12
+ ["image/jpeg", ".jpg"],
13
+ ["image/gif", ".gif"],
14
+ ["image/webp", ".webp"],
15
+ ["image/avif", ".avif"]
16
+ ]);
17
+ const TYPE_NAMES = new Map([
18
+ ["image/svg+xml", "SVG"],
19
+ ["image/bmp", "BMP"],
20
+ ["image/x-ms-bmp", "BMP"],
21
+ ["image/tiff", "TIFF"],
22
+ ["image/heic", "HEIC"],
23
+ ["image/x-icon", "ICO"],
24
+ ["image/vnd.microsoft.icon", "ICO"]
25
+ ]);
26
+ /** What an upload of another type is told, naming the type it sent (`mime`, already lowercased, without parameters). */
27
+ export function unsupportedTypeMessage(mime) {
28
+ const named = TYPE_NAMES.get(mime) ?? (/^[a-z0-9][a-z0-9.+-]{0,60}\/[a-z0-9][a-z0-9.+-]{0,60}$/.test(mime) ? mime : mime ? "that type" : "a file without a type");
29
+ return `Orbit takes PNG, JPEG, GIF, WebP and AVIF images, not ${named}.`;
30
+ }
31
+ /** What a file Orbit won't show is stored and served as: bytes to download, never a page. */
32
+ export const DOWNLOAD_TYPE = "application/octet-stream";
33
+ /** True for a type Orbit shows as an image; any other attachment is a file to download. */
34
+ export function isShownImage(mime) {
35
+ return IMAGE_EXTENSIONS.has(mime);
36
+ }