@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,334 @@
1
+ /**
2
+ * The OrbitClient over HTTP: `createHttpClient({ baseUrl })` calls an Orbit's
3
+ * JSON API with any `fetch` (remote agents, the CLI in remote mode, the web
4
+ * app, the Suite). Each method is one route; a refusal comes back as the
5
+ * OrbitError the server answered with.
6
+ *
7
+ * A command session's board lives here, on the caller's side: the server
8
+ * forgets between requests, so each command sends the board this client
9
+ * chose last, and the caller's orbit.json as it reads it.
10
+ */
11
+ import { ERROR_CODES, OrbitError } from "../core/errors.js";
12
+ import { DOWNLOAD_TYPE, isShownImage } from "../core/image-types.js";
13
+ import { objectUrl } from "./object-url.js";
14
+ function repoFields(repo) {
15
+ if (!repo)
16
+ return {};
17
+ const fields = {};
18
+ if (repo.board)
19
+ fields.repo_board = repo.board;
20
+ if (repo.problem)
21
+ fields.repo_board_problem = repo.problem;
22
+ if (repo.problem === "not_slug" && typeof repo.value === "string")
23
+ fields.repo_board = repo.value;
24
+ return fields;
25
+ }
26
+ const hintQuery = (hint = {}) => ({ board_id: hint.board_id, board: hint.board });
27
+ export function createHttpClient(options) {
28
+ const base = options.baseUrl.replace(/\/+$/, "");
29
+ const doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
30
+ let sessionBoard = options.board ?? null;
31
+ async function send(method, path, call = {}) {
32
+ const url = new URL(`${base}${path}`);
33
+ for (const [key, value] of Object.entries(call.query ?? {})) {
34
+ if (value !== undefined && value !== null && value !== "" && value !== false)
35
+ url.searchParams.set(key, typeof value === "string" ? value : JSON.stringify(value));
36
+ }
37
+ const headers = { ...options.headers };
38
+ let body;
39
+ if (call.raw) {
40
+ headers["Content-Type"] = call.raw.type;
41
+ if (call.raw.name)
42
+ headers["X-File-Name"] = call.raw.name;
43
+ body = call.raw.bytes;
44
+ }
45
+ else if (call.body !== undefined) {
46
+ headers["Content-Type"] = "application/json";
47
+ body = JSON.stringify(call.body);
48
+ }
49
+ const response = await doFetch(url, { method, headers, body });
50
+ if (!(call.ok ?? [200, 201, 202]).includes(response.status)) {
51
+ const text = await response.text();
52
+ let payload = {};
53
+ try {
54
+ payload = JSON.parse(text);
55
+ }
56
+ catch {
57
+ payload = { message: text };
58
+ }
59
+ const code = typeof payload.error === "string" ? payload.error : "";
60
+ const message = typeof payload.message === "string" && payload.message ? payload.message : code || `Orbit API ${method} ${url.pathname} failed with HTTP ${response.status}`;
61
+ if (code in ERROR_CODES)
62
+ throw new OrbitError(code, { message });
63
+ throw new Error(message);
64
+ }
65
+ return response;
66
+ }
67
+ async function request(method, path, call = {}) {
68
+ const response = await send(method, path, call);
69
+ const text = await response.text();
70
+ return (text ? JSON.parse(text) : {});
71
+ }
72
+ const id = encodeURIComponent;
73
+ const get = (path, query) => request("GET", path, query ? { query } : {});
74
+ const ticket = (ticketId, rest = "") => `/api/tickets/${id(ticketId)}${rest}`;
75
+ const contextQuery = (opts = {}, hint) => ({
76
+ ...hintQuery(hint),
77
+ max_chars_per_field: opts.max_chars_per_field,
78
+ comment_limit: opts.comment_limit,
79
+ include_parent_full: opts.include_parent_full ? "true" : undefined,
80
+ include_related_full: opts.include_related_full ? "true" : undefined
81
+ });
82
+ const contentPath = (ticketId, attachmentId) => ticket(ticketId, `/attachments/${id(attachmentId)}/content`);
83
+ return {
84
+ health: () => get("/api/health"),
85
+ version: (opts = {}) => get("/api/version", { refresh: opts.refresh ? "1" : undefined }),
86
+ update: () => request("POST", "/api/update"),
87
+ bootstrap: (hint) => get("/api/bootstrap", hintQuery(hint)),
88
+ sessions: (opts = {}) => get("/api/sessions", { since: opts.since }),
89
+ async command(input) {
90
+ const body = {
91
+ command: input.command,
92
+ argv: input.argv,
93
+ text: input.text,
94
+ json: Boolean(input.json),
95
+ read_only: Boolean(input.readOnly),
96
+ board: sessionBoard,
97
+ transport: input.transport ?? "mcp",
98
+ ...repoFields(options.repo?.() ?? null)
99
+ };
100
+ const reply = await request("POST", "/api/cmd", { body, ok: [200, 422] });
101
+ if (reply.board)
102
+ sessionBoard = reply.board;
103
+ return reply;
104
+ },
105
+ search: (hint, q) => get("/api/search", {
106
+ ...hintQuery(hint),
107
+ q: q.q,
108
+ limit: q.limit,
109
+ mode: q.mode,
110
+ include_full: q.include_full ? "true" : undefined,
111
+ fields: Array.isArray(q.fields) ? q.fields.join(",") : q.fields,
112
+ max_chars_per_field: q.max_chars_per_field
113
+ }),
114
+ boards: {
115
+ list: () => get("/api/boards"),
116
+ create: (input) => request("POST", "/api/boards", { body: input }),
117
+ update: (board, patch) => request("PATCH", `/api/boards/${id(board)}`, { body: patch }),
118
+ remove: (board, input) => request("DELETE", `/api/boards/${id(board)}`, { body: input }),
119
+ context: (board, opts = {}) => get(`/api/boards/${id(board)}/context`, { include_struck: opts.include_struck ? "true" : undefined }),
120
+ archive: (board) => get(`/api/boards/${id(board)}/archive`),
121
+ export: (board, opts = {}) => get(`/api/boards/${id(board)}/export`, { include_attachments: opts.include_attachments ? "true" : undefined }),
122
+ import: (input) => request("POST", "/api/admin/import", { body: input })
123
+ },
124
+ states: {
125
+ create: (board, input) => request("POST", `/api/boards/${id(board)}/states`, { body: input }),
126
+ update: (stateId, patch, hint) => request("PATCH", `/api/states/${id(stateId)}`, { body: patch, query: hintQuery(hint) }),
127
+ reorder: (board, input) => request("PATCH", `/api/boards/${id(board)}/states`, { body: input }),
128
+ remove: (stateId, hint) => request("DELETE", `/api/states/${id(stateId)}`, { query: hintQuery(hint) })
129
+ },
130
+ tickets: {
131
+ create: (input) => request("POST", "/api/tickets", { body: input }),
132
+ lookup: (hint, by) => get("/api/tickets/lookup", { ...hintQuery(hint), number: by.number, title: by.title }),
133
+ read: (ticketId, hint) => get(ticket(ticketId), hintQuery(hint)),
134
+ update: (ticketId, patch, hint) => request("PATCH", ticket(ticketId), { body: patch, query: hintQuery(hint) }),
135
+ archive: (ticketId, hint) => request("POST", ticket(ticketId, "/archive"), { query: hintQuery(hint) }),
136
+ restore: (ticketId, hint) => request("POST", ticket(ticketId, "/restore"), { query: hintQuery(hint) }),
137
+ remove: (ticketId, hint) => request("DELETE", ticket(ticketId), { query: hintQuery(hint) }),
138
+ context: (ticketId, opts, hint) => get(ticket(ticketId, "/context"), contextQuery(opts, hint)),
139
+ contextFull: (ticketId, opts, hint) => get(ticket(ticketId, "/context/full"), contextQuery(opts, hint)),
140
+ dispatchPacket: (ticketId, opts, hint) => get(ticket(ticketId, "/dispatch-packet"), contextQuery(opts, hint)),
141
+ history: (ticketId, hint) => get(ticket(ticketId, "/history"), hintQuery(hint)),
142
+ relations: (ticketId, hint) => get(ticket(ticketId, "/relations"), hintQuery(hint)),
143
+ blockers: (ticketId, hint) => get(ticket(ticketId, "/blockers"), hintQuery(hint))
144
+ },
145
+ comments: {
146
+ list: (ticketId, hint) => get(ticket(ticketId, "/comments"), hintQuery(hint)),
147
+ create: (ticketId, input, hint) => request("POST", ticket(ticketId, "/comments"), { body: input, query: hintQuery(hint) }),
148
+ update: (ticketId, commentId, patch, hint) => request("PATCH", ticket(ticketId, `/comments/${id(commentId)}`), { body: patch, query: hintQuery(hint) })
149
+ },
150
+ attachments: {
151
+ list: (ticketId, hint) => get(ticket(ticketId, "/attachments"), hintQuery(hint)),
152
+ create: (ticketId, bytes, meta, hint) => {
153
+ const raw = { bytes, type: meta.mime_type };
154
+ if (meta.original_name)
155
+ raw.name = meta.original_name;
156
+ return request("POST", ticket(ticketId, "/attachments"), { raw, query: hintQuery(hint) });
157
+ },
158
+ async content(ticketId, attachmentId, hint) {
159
+ const response = await send("GET", contentPath(ticketId, attachmentId), { query: hintQuery(hint) });
160
+ const mime = response.headers.get("content-type") ?? DOWNLOAD_TYPE;
161
+ return { bytes: new Uint8Array(await response.arrayBuffer()), mime_type: mime, download_only: !isShownImage(mime), original_name: dispositionName(response.headers.get("content-disposition")) };
162
+ },
163
+ async url(ticketId, attachmentId, hint) {
164
+ if (options.images === "object") {
165
+ const response = await send("GET", contentPath(ticketId, attachmentId), { query: hintQuery(hint) });
166
+ return objectUrl(new Uint8Array(await response.arrayBuffer()), response.headers.get("content-type") ?? "application/octet-stream");
167
+ }
168
+ const url = new URL(`${base}${contentPath(ticketId, attachmentId)}`);
169
+ for (const [key, value] of Object.entries(hintQuery(hint)))
170
+ if (typeof value === "string" && value)
171
+ url.searchParams.set(key, value);
172
+ return { url: url.href, release: () => undefined };
173
+ },
174
+ remove: (ticketId, attachmentId, hint) => request("DELETE", ticket(ticketId, `/attachments/${id(attachmentId)}`), { query: hintQuery(hint) })
175
+ },
176
+ artifacts: {
177
+ read: (ticketId, path, hint) => get(ticket(ticketId, "/artifacts/markdown"), { ...hintQuery(hint), path }),
178
+ write: (ticketId, input, hint) => request("POST", ticket(ticketId, "/artifacts/markdown"), { body: input, query: hintQuery(hint) })
179
+ },
180
+ verdicts: {
181
+ list: (ticketId, opts = {}, hint) => get(ticket(ticketId, "/review-verdicts"), { ...hintQuery(hint), limit: opts.limit }),
182
+ create: (ticketId, input, hint) => request("POST", ticket(ticketId, "/review-verdicts"), { body: input, query: hintQuery(hint) }),
183
+ get: (reviewId, hint) => get(`/api/review-verdicts/${id(reviewId)}`, hintQuery(hint))
184
+ },
185
+ entries: {
186
+ create: (board, input) => request("POST", `/api/boards/${id(board)}/entries`, { body: input }),
187
+ update: (entryId, patch, hint) => request("PATCH", `/api/board-entries/${id(entryId)}`, { body: patch, query: hintQuery(hint) }),
188
+ remove: (entryId, hint) => request("DELETE", `/api/board-entries/${id(entryId)}`, { query: hintQuery(hint) })
189
+ },
190
+ relations: {
191
+ create: (input) => request("POST", "/api/relations", { body: input }),
192
+ remove: (relationId, hint) => request("DELETE", `/api/relations/${id(relationId)}`, { query: hintQuery(hint) })
193
+ },
194
+ agent: {
195
+ claimNext: (hint, filters = {}) => request("POST", "/api/agent/claim-next", { body: { ...filters, ...hintQuery(hint) } }),
196
+ claim: (input) => request("POST", "/api/agent/claim", { body: input }),
197
+ checkpoint: (input) => request("POST", "/api/agent/checkpoint", { body: input }),
198
+ complete: (input) => request("POST", "/api/agent/complete", { body: input })
199
+ },
200
+ changes(boardId, listener, opts = {}) {
201
+ const controller = new AbortController();
202
+ void followChanges({
203
+ doFetch,
204
+ url: `${base}/api/events/stream?board_id=${id(boardId)}`,
205
+ headers: options.headers ?? {},
206
+ signal: controller.signal,
207
+ listener,
208
+ onState: opts.onState ?? (() => undefined),
209
+ firstMs: options.reconnect?.firstMs ?? 1000,
210
+ maxMs: options.reconnect?.maxMs ?? 30_000,
211
+ staleMs: options.staleMs ?? 75_000
212
+ });
213
+ return () => {
214
+ controller.abort();
215
+ };
216
+ }
217
+ };
218
+ }
219
+ /** Resolves after `ms`, or at once when `signal` aborts. */
220
+ function pause(ms, signal) {
221
+ return new Promise((resolve) => {
222
+ const timer = setTimeout(done, ms);
223
+ function done() {
224
+ clearTimeout(timer);
225
+ signal.removeEventListener("abort", done);
226
+ resolve();
227
+ }
228
+ signal.addEventListener("abort", done);
229
+ });
230
+ }
231
+ /**
232
+ * Follows a board's server-sent event stream until aborted: each change to
233
+ * `listener`, in order. When the stream drops it waits (1 s, doubling to
234
+ * 30 s) and reconnects with `Last-Event-ID`, so the server replays what was
235
+ * missed. Its state goes to `onState`: connecting, then live, then down
236
+ * while it is lost (and retrying), live again once back. A board that is
237
+ * gone (404) ends it, down.
238
+ */
239
+ async function followChanges(f) {
240
+ let lastId = null;
241
+ let delay = f.firstMs;
242
+ let state = null;
243
+ const report = (next) => {
244
+ if (next === state || f.signal.aborted)
245
+ return;
246
+ state = next;
247
+ f.onState(next);
248
+ };
249
+ const aborted = () => f.signal.aborted;
250
+ report("connecting");
251
+ while (!aborted()) {
252
+ const attempt = new AbortController();
253
+ const stop = () => {
254
+ attempt.abort();
255
+ };
256
+ f.signal.addEventListener("abort", stop);
257
+ const quiet = { timer: null };
258
+ const watchdog = () => {
259
+ if (quiet.timer)
260
+ clearTimeout(quiet.timer);
261
+ quiet.timer = setTimeout(stop, f.staleMs);
262
+ };
263
+ try {
264
+ const headers = { ...f.headers, Accept: "text/event-stream" };
265
+ if (lastId)
266
+ headers["Last-Event-ID"] = lastId;
267
+ watchdog();
268
+ const response = await f.doFetch(f.url, { headers, signal: attempt.signal });
269
+ if (response.status === 404) {
270
+ report("down");
271
+ return;
272
+ }
273
+ if (!response.ok || !response.body)
274
+ throw new Error(`event stream answered ${response.status}`);
275
+ report("live");
276
+ delay = f.firstMs;
277
+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
278
+ let buffer = "";
279
+ for (;;) {
280
+ const { done, value } = await reader.read();
281
+ if (done)
282
+ break;
283
+ watchdog();
284
+ buffer += value;
285
+ let end;
286
+ while ((end = buffer.indexOf("\n\n")) >= 0) {
287
+ const frame = buffer.slice(0, end);
288
+ buffer = buffer.slice(end + 2);
289
+ let data = "";
290
+ let frameId = null;
291
+ for (const line of frame.split("\n")) {
292
+ if (line.startsWith("data: "))
293
+ data += (data ? "\n" : "") + line.slice(6);
294
+ else if (line.startsWith("id: "))
295
+ frameId = line.slice(4);
296
+ }
297
+ if (!data)
298
+ continue;
299
+ if (frameId)
300
+ lastId = frameId;
301
+ f.listener(JSON.parse(data));
302
+ }
303
+ }
304
+ }
305
+ catch {
306
+ // Dropped, refused or gone quiet: wait, then reconnect.
307
+ }
308
+ finally {
309
+ if (quiet.timer)
310
+ clearTimeout(quiet.timer);
311
+ f.signal.removeEventListener("abort", stop);
312
+ }
313
+ if (aborted())
314
+ return;
315
+ report("down");
316
+ await pause(delay, f.signal);
317
+ delay = Math.min(delay * 2, f.maxMs);
318
+ }
319
+ }
320
+ /** The file name a `Content-Disposition` gives (its UTF-8 form first), or "". */
321
+ function dispositionName(value) {
322
+ if (!value)
323
+ return "";
324
+ const utf8 = /filename\*=UTF-8''([^;]+)/i.exec(value);
325
+ if (utf8?.[1]) {
326
+ try {
327
+ return decodeURIComponent(utf8[1]);
328
+ }
329
+ catch {
330
+ // Fall back to the plain name.
331
+ }
332
+ }
333
+ return /filename="([^"]*)"/i.exec(value)?.[1] ?? "";
334
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The in-process OrbitClient: every use case, acting as one actor, in this
3
+ * process (the CLI, `orbit mcp` on a computer, the HTTP app's routes). Each
4
+ * method finds its board the way the JSON API always has: a route naming a
5
+ * board uses it; a record named by id is looked for on the hinted board
6
+ * first, then on the others; a route that needs a board and gets no hint is
7
+ * refused with `board_id_required`.
8
+ */
9
+ import type { Ctx } from "../core/context.ts";
10
+ import type { Id } from "../core/model.ts";
11
+ import type { RepoSource, Transport } from "../commands/index.ts";
12
+ import type { HostServices, OrbitClient } from "./types.ts";
13
+ export interface LocalClientOptions {
14
+ transport?: Transport;
15
+ /** A command over HTTP to an Orbit others use. */
16
+ shared?: boolean;
17
+ /** The board the command session starts on (ORBIT_BOARD). */
18
+ board?: string | null;
19
+ /** Where the caller's orbit.json comes from. */
20
+ repo?: RepoSource | null;
21
+ /** Every board's committed changes, from any process. */
22
+ watch: (boardId: Id, listener: Parameters<OrbitClient["changes"]>[1]) => () => void;
23
+ /** What the computer can do: the version check, self-update. */
24
+ host?: HostServices;
25
+ }
26
+ export declare function createLocalClient(c: Ctx, options: LocalClientOptions): OrbitClient;
@@ -0,0 +1,190 @@
1
+ /**
2
+ * The in-process OrbitClient: every use case, acting as one actor, in this
3
+ * process (the CLI, `orbit mcp` on a computer, the HTTP app's routes). Each
4
+ * method finds its board the way the JSON API always has: a route naming a
5
+ * board uses it; a record named by id is looked for on the hinted board
6
+ * first, then on the others; a route that needs a board and gets no hint is
7
+ * refused with `board_id_required`.
8
+ */
9
+ import { checkpointTicket, claimNext, claimTicket, completeTicket, contextPack, dispatchPacket, readComments, readTicket } from "../core/agent.js";
10
+ import { archivedTickets, boardManual, createBoard, createEntry, deleteBoard, deleteEntry, listBoards, strikeEntry, updateBoard } from "../core/boards.js";
11
+ import { bootstrap } from "../core/bootstrap.js";
12
+ import { boardFromHints, boardHolding, onBoard } from "../core/context.js";
13
+ import { OrbitError } from "../core/errors.js";
14
+ import { attachmentContent, createAttachment, deleteAttachment, listAttachments, readArtifact, writeArtifact } from "../core/files.js";
15
+ import { createRelation, deleteRelation, ticketBlockers, ticketRelations } from "../core/relations.js";
16
+ import { boardHoldingCrossLink, linkAcrossBoards, unlinkAcrossBoards } from "../core/cross-board.js";
17
+ import { searchTickets } from "../core/search.js";
18
+ import { listSessions } from "../core/sessions.js";
19
+ import { importAsNewBoard, importReplacing, exportBoard, refuseAgentReplace } from "../core/snapshots.js";
20
+ import { createState, deleteState, reorderStates, updateState } from "../core/states.js";
21
+ import { asText } from "../core/text.js";
22
+ import { archiveTicket, createComment, createTicket, deleteTicket, restoreTicket, setCommentHidden, ticketHistory, updateTicket } from "../core/tickets.js";
23
+ import { createVerdict, getVerdict, listVerdicts } from "../core/verdicts.js";
24
+ import { objectUrl } from "./object-url.js";
25
+ export function createLocalClient(c, options) {
26
+ // The command language loads with the first command, so a host that never runs one (the browser edition) never loads it.
27
+ const commands = () => import("../commands/index.js");
28
+ let session = null;
29
+ const commandSession = () => (session ??= commands().then(({ createSession }) => createSession(c, { transport: options.transport ?? "mcp", shared: options.shared ?? false, board: options.board ?? null, repo: options.repo ?? null })));
30
+ const hinted = (hint) => boardFromHints(c, hint?.board_id, hint?.board);
31
+ /** The board a route needs: the hinted one, else `board_id_required`. */
32
+ async function required(hint) {
33
+ const row = await hinted(hint);
34
+ if (!row)
35
+ throw new OrbitError("board_id_required");
36
+ return onBoard(c, row);
37
+ }
38
+ /** The board a path names by id or slug, marked as used (the app opens it). */
39
+ /** A board as the list has it, by id or slug, without opening its data. */
40
+ async function listed(ident) {
41
+ const row = (await c.store.boards.get(ident)) ?? (await c.store.boards.bySlug(ident));
42
+ if (!row)
43
+ throw new OrbitError("board_not_found");
44
+ return row;
45
+ }
46
+ async function named(ident, touch = true) {
47
+ const row = await listed(ident);
48
+ if (touch)
49
+ await c.store.boards.touch(row.id, c.clock.now());
50
+ return onBoard(c, row);
51
+ }
52
+ const ticketBoard = async (id, hint) => boardHolding(c, "ticket", id, await hinted(hint), "ticket_not_found");
53
+ /** The hinted board, else the board holding `ticketId`, else `board_id_required`. */
54
+ async function hintedOrHolding(hint, ticketId) {
55
+ const row = await hinted(hint);
56
+ if (row)
57
+ return onBoard(c, row);
58
+ const id = asText(ticketId);
59
+ if (id)
60
+ return boardHolding(c, "ticket", id, null, "ticket_not_found");
61
+ throw new OrbitError("board_id_required");
62
+ }
63
+ const hintOf = (input) => ({ board: input.board ?? null, board_id: input.board_id ?? null });
64
+ return {
65
+ health: () => Promise.resolve({ ok: true }),
66
+ async version(opts = {}) {
67
+ const check = options.host?.version;
68
+ if (!check)
69
+ throw new OrbitError("not_found");
70
+ return check({ force: Boolean(opts.refresh) });
71
+ },
72
+ update() {
73
+ const started = options.host?.selfUpdate?.() ?? null;
74
+ return started ? Promise.resolve(started) : Promise.reject(new OrbitError("not_found"));
75
+ },
76
+ bootstrap: (hint = {}) => bootstrap(c, { board_id: hint.board_id ?? null, board: hint.board ?? null }),
77
+ sessions: (opts = {}) => listSessions(c, opts),
78
+ command: async (input) => {
79
+ const [s, { runCommand }] = await Promise.all([commandSession(), commands()]);
80
+ if (input.transport)
81
+ s.transport = input.transport;
82
+ return runCommand(input, s);
83
+ },
84
+ search: async (hint, query) => searchTickets(await required(hint), query),
85
+ boards: {
86
+ list: () => listBoards(c),
87
+ create: (input) => createBoard(c, input),
88
+ update: async (board, patch) => updateBoard(await named(board), patch),
89
+ // Without opening its data: a board whose file is gone can still be deleted.
90
+ remove: async (board, input) => deleteBoard(c, await listed(board), input),
91
+ context: async (board, opts = {}) => boardManual(await named(board), { includeStruck: Boolean(opts.include_struck) }),
92
+ archive: async (board) => ({ tickets: await archivedTickets(await named(board)) }),
93
+ export: async (board, opts = {}) => exportBoard(await named(board), { includeAttachments: Boolean(opts.include_attachments) }),
94
+ import: async (input) => {
95
+ // First, so an agent asking to replace is told so before its board is looked up, whatever else it asks.
96
+ refuseAgentReplace(c, input);
97
+ if (input.create_new)
98
+ return importAsNewBoard(c, input);
99
+ return importReplacing(await required(hintOf(input)), input);
100
+ }
101
+ },
102
+ states: {
103
+ create: async (board, input) => createState(await named(board), input),
104
+ update: async (id, patch, hint) => updateState(await boardHolding(c, "state", id, await hinted(hint), "state_not_found"), id, patch),
105
+ reorder: async (board, input) => reorderStates(await named(board), input),
106
+ remove: async (id, hint) => deleteState(await boardHolding(c, "state", id, await hinted(hint), "state_not_found"), id)
107
+ },
108
+ tickets: {
109
+ create: async (input) => createTicket(await required(hintOf(input)), input),
110
+ lookup: async (hint, by) => {
111
+ const lookup = {};
112
+ if (by.number !== undefined && by.number !== null)
113
+ lookup.number = by.number;
114
+ if (by.title !== undefined && by.title !== null)
115
+ lookup.title = by.title;
116
+ return readTicket(await required(hint), lookup);
117
+ },
118
+ read: async (id, hint) => readTicket(await ticketBoard(id, hint), { ticket_id: id }),
119
+ update: async (id, patch, hint) => updateTicket(await ticketBoard(id, hint), id, patch),
120
+ archive: async (id, hint) => archiveTicket(await ticketBoard(id, hint), id),
121
+ restore: async (id, hint) => restoreTicket(await ticketBoard(id, hint), id),
122
+ remove: async (id, hint) => deleteTicket(await ticketBoard(id, hint), id),
123
+ context: async (id, opts = {}, hint) => contextPack(await ticketBoard(id, hint), id, opts),
124
+ contextFull: async (id, opts = {}, hint) => contextPack(await ticketBoard(id, hint), id, opts, true),
125
+ dispatchPacket: async (id, opts = {}, hint) => dispatchPacket(await ticketBoard(id, hint), id, opts),
126
+ history: async (id, hint) => ticketHistory(await ticketBoard(id, hint), id),
127
+ relations: async (id, hint) => ticketRelations(await ticketBoard(id, hint), id),
128
+ blockers: async (id, hint) => ticketBlockers(await ticketBoard(id, hint), id)
129
+ },
130
+ comments: {
131
+ list: async (ticketId, hint) => readComments(await ticketBoard(ticketId, hint), { ticket_id: ticketId }),
132
+ create: async (ticketId, input, hint) => createComment(await ticketBoard(ticketId, hint), ticketId, input),
133
+ update: async (ticketId, commentId, patch, hint) => setCommentHidden(await ticketBoard(ticketId, hint), ticketId, commentId, patch)
134
+ },
135
+ attachments: {
136
+ list: async (ticketId, hint) => listAttachments(await ticketBoard(ticketId, hint), ticketId),
137
+ create: async (ticketId, bytes, meta, hint) => createAttachment(await ticketBoard(ticketId, hint), ticketId, { bytes, mime_type: meta.mime_type, original_name: meta.original_name }),
138
+ content: async (ticketId, attachmentId, hint) => attachmentContent(await ticketBoard(ticketId, hint), ticketId, attachmentId),
139
+ url: async (ticketId, attachmentId, hint) => {
140
+ const file = await attachmentContent(await ticketBoard(ticketId, hint), ticketId, attachmentId);
141
+ return objectUrl(file.bytes, file.mime_type);
142
+ },
143
+ remove: async (ticketId, attachmentId, hint) => deleteAttachment(await ticketBoard(ticketId, hint), ticketId, attachmentId)
144
+ },
145
+ artifacts: {
146
+ read: async (ticketId, path, hint) => readArtifact(await ticketBoard(ticketId, hint), ticketId, path),
147
+ write: async (ticketId, input, hint) => writeArtifact(await ticketBoard(ticketId, hint), ticketId, input)
148
+ },
149
+ verdicts: {
150
+ list: async (ticketId, opts = {}, hint) => ({ review_verdicts: await listVerdicts(await ticketBoard(ticketId, hint), ticketId, opts) }),
151
+ create: async (ticketId, input, hint) => createVerdict(await ticketBoard(ticketId, hint), ticketId, input),
152
+ get: async (id, hint) => getVerdict(await required(hint), id)
153
+ },
154
+ entries: {
155
+ create: async (board, input) => createEntry(await named(board), input),
156
+ update: async (id, patch, hint) => strikeEntry(await boardHolding(c, "entry", id, await hinted(hint), "board_entry_not_found"), id, patch),
157
+ remove: async (id, hint) => deleteEntry(await boardHolding(c, "entry", id, await hinted(hint), "board_entry_not_found"), id)
158
+ },
159
+ relations: {
160
+ create: async (input) => {
161
+ const from = await hintedOrHolding(hintOf(input), input.source_ticket_id);
162
+ const other = typeof input.target_board === "string" && input.target_board ? await listed(input.target_board) : null;
163
+ if (!other || other.id === from.board.id)
164
+ return createRelation(from, input);
165
+ return linkAcrossBoards(c, from, asText(input.source_ticket_id), await onBoard(c, other), asText(input.target_ticket_id), input.type);
166
+ },
167
+ remove: async (id, hint) => {
168
+ const board = await hinted(hint);
169
+ // A link on one board, else one across boards (each lives on one board).
170
+ const holder = (await c.store.locate("relation", id, board?.id)) ?? null;
171
+ if (holder)
172
+ return deleteRelation(await onBoard(c, holder), id);
173
+ const crossHolder = await boardHoldingCrossLink(c, id, board);
174
+ if (!crossHolder)
175
+ throw new OrbitError("relation_not_found");
176
+ return unlinkAcrossBoards(await onBoard(c, crossHolder), id);
177
+ }
178
+ },
179
+ agent: {
180
+ claimNext: async (hint, filters = {}) => claimNext(await required(hint), filters),
181
+ claim: async (input) => claimTicket(await hintedOrHolding(hintOf(input), input.ticket_id), input),
182
+ checkpoint: async (input) => checkpointTicket(await hintedOrHolding(hintOf(input), input.ticket_id), input),
183
+ complete: async (input) => completeTicket(await hintedOrHolding(hintOf(input), input.ticket_id), input)
184
+ },
185
+ changes: (boardId, listener, opts = {}) => {
186
+ opts.onState?.("live");
187
+ return options.watch(boardId, listener);
188
+ }
189
+ };
190
+ }
@@ -0,0 +1,8 @@
1
+ import type { ImageUrl } from "./types.ts";
2
+ /**
3
+ * Bytes as an object URL, which `release()` revokes (once). A `blob:` URL
4
+ * runs in the page's own origin, so only a type Orbit shows as an image keeps
5
+ * its type; anything else (an SVG, which can carry script) becomes bytes to
6
+ * download, which a browser never renders as a page.
7
+ */
8
+ export declare function objectUrl(bytes: Uint8Array, type: string): ImageUrl;
@@ -0,0 +1,21 @@
1
+ import { DOWNLOAD_TYPE, isShownImage } from "../core/image-types.js";
2
+ /**
3
+ * Bytes as an object URL, which `release()` revokes (once). A `blob:` URL
4
+ * runs in the page's own origin, so only a type Orbit shows as an image keeps
5
+ * its type; anything else (an SVG, which can carry script) becomes bytes to
6
+ * download, which a browser never renders as a page.
7
+ */
8
+ export function objectUrl(bytes, type) {
9
+ const safe = isShownImage((type.split(";")[0] ?? "").trim().toLowerCase()) ? type : DOWNLOAD_TYPE;
10
+ const url = URL.createObjectURL(new Blob([bytes], { type: safe }));
11
+ let released = false;
12
+ return {
13
+ url,
14
+ release() {
15
+ if (released)
16
+ return;
17
+ released = true;
18
+ URL.revokeObjectURL(url);
19
+ }
20
+ };
21
+ }