@starlove/orbit 1.4.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/Dockerfile +10 -8
  2. package/README.md +146 -164
  3. package/dist/full/csp.txt +1 -0
  4. package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-subset.woff2 +0 -0
  5. package/dist/full/fonts/JetBrains_Mono/README.txt +5 -0
  6. package/dist/full/fonts/Mona_Sans/MonaSans-subset.woff2 +0 -0
  7. package/{public/fonts/Open_Sans → dist/full/fonts/Mona_Sans}/OFL.txt +93 -93
  8. package/dist/full/fonts/Mona_Sans/README.txt +10 -0
  9. package/{public/fonts/JetBrains_Mono → dist/full/fonts/Newsreader}/OFL.txt +93 -93
  10. package/dist/full/fonts/Newsreader/README.txt +8 -0
  11. package/dist/full/fonts/Newsreader/newsreader.woff2 +0 -0
  12. package/dist/full/index.html +36 -653
  13. package/dist/full/js/app.BmuponPJ.js +32 -0
  14. package/dist/full/js/app.BmuponPJ.js.map +1 -0
  15. package/dist/full/js/chunk.1fJGQGbW.js +2 -0
  16. package/dist/full/js/chunk.1fJGQGbW.js.map +1 -0
  17. package/dist/full/js/chunk.5D7JO2zp.js +2 -0
  18. package/dist/full/js/chunk.5D7JO2zp.js.map +1 -0
  19. package/dist/full/js/chunk.BrNn_D79.js +2 -0
  20. package/dist/full/js/chunk.BrNn_D79.js.map +1 -0
  21. package/dist/full/js/chunk.CokJXIqM.js +2 -0
  22. package/dist/full/js/chunk.CokJXIqM.js.map +1 -0
  23. package/dist/full/js/chunk.D_Haqy6R.js +2 -0
  24. package/dist/full/js/chunk.D_Haqy6R.js.map +1 -0
  25. package/dist/full/js/chunk.RO0jsH6I.js +15 -0
  26. package/dist/full/js/chunk.RO0jsH6I.js.map +1 -0
  27. package/dist/full/js/chunk.dqWOEtBM.js +2 -0
  28. package/dist/full/js/chunk.dqWOEtBM.js.map +1 -0
  29. package/dist/full/js/warm.f260bbbc.js +15 -0
  30. package/dist/full/orbits-favicon.png +0 -0
  31. package/dist/full/orbits-logo-bg-wide.png +0 -0
  32. package/dist/full/orbits-logo-bg.png +0 -0
  33. package/dist/full/orbits-logo.png +0 -0
  34. package/dist/full/styles.2b154ffe.css +1 -0
  35. package/dist/full/vendor/starlove-ui/theme-init.js +66 -0
  36. package/dist/full/version.json +2 -2
  37. package/docs/AGENT_GUIDE.md +114 -0
  38. package/docs/UPDATING.md +249 -64
  39. package/lib/adapters/indexeddb/blobs.d.ts +3 -0
  40. package/lib/adapters/indexeddb/blobs.js +54 -0
  41. package/lib/adapters/indexeddb/database.d.ts +60 -0
  42. package/lib/adapters/indexeddb/database.js +126 -0
  43. package/lib/adapters/indexeddb/index.d.ts +5 -0
  44. package/lib/adapters/indexeddb/index.js +3 -0
  45. package/lib/adapters/indexeddb/locks.d.ts +14 -0
  46. package/lib/adapters/indexeddb/locks.js +40 -0
  47. package/lib/adapters/indexeddb/store.d.ts +40 -0
  48. package/lib/adapters/indexeddb/store.js +424 -0
  49. package/lib/adapters/indexeddb/tables.d.ts +43 -0
  50. package/lib/adapters/indexeddb/tables.js +127 -0
  51. package/lib/adapters/memory/blobs.d.ts +3 -0
  52. package/lib/adapters/memory/blobs.js +44 -0
  53. package/lib/adapters/memory/board.d.ts +41 -0
  54. package/lib/adapters/memory/board.js +521 -0
  55. package/lib/adapters/memory/index.d.ts +2 -0
  56. package/lib/adapters/memory/index.js +2 -0
  57. package/lib/adapters/memory/search.d.ts +6 -0
  58. package/lib/adapters/memory/search.js +30 -0
  59. package/lib/adapters/memory/store.d.ts +34 -0
  60. package/lib/adapters/memory/store.js +266 -0
  61. package/lib/adapters/sqlite/backups.d.ts +53 -0
  62. package/lib/adapters/sqlite/backups.js +232 -0
  63. package/lib/adapters/sqlite/blobs.d.ts +14 -0
  64. package/lib/adapters/sqlite/blobs.js +109 -0
  65. package/lib/adapters/sqlite/board.d.ts +77 -0
  66. package/lib/adapters/sqlite/board.js +328 -0
  67. package/lib/adapters/sqlite/database.d.ts +67 -0
  68. package/lib/adapters/sqlite/database.js +285 -0
  69. package/lib/adapters/sqlite/index.d.ts +3 -0
  70. package/lib/adapters/sqlite/index.js +2 -0
  71. package/lib/adapters/sqlite/lock.d.ts +11 -0
  72. package/lib/adapters/sqlite/lock.js +34 -0
  73. package/lib/adapters/sqlite/records.d.ts +54 -0
  74. package/lib/adapters/sqlite/records.js +546 -0
  75. package/lib/adapters/sqlite/schema.d.ts +54 -0
  76. package/lib/adapters/sqlite/schema.js +276 -0
  77. package/lib/adapters/sqlite/store.d.ts +39 -0
  78. package/lib/adapters/sqlite/store.js +385 -0
  79. package/lib/browser/in-page.d.ts +31 -0
  80. package/lib/browser/in-page.js +46 -0
  81. package/lib/browser/old-preview.d.ts +13 -0
  82. package/lib/browser/old-preview.js +80 -0
  83. package/lib/browser/preview-board.d.ts +8 -0
  84. package/lib/browser/preview-board.js +69 -0
  85. package/lib/cli/main.d.ts +7 -0
  86. package/lib/cli/main.js +343 -0
  87. package/lib/cli/orbit.d.ts +8 -0
  88. package/lib/cli/orbit.js +25 -0
  89. package/lib/client/http.d.ts +38 -0
  90. package/lib/client/http.js +334 -0
  91. package/lib/client/local.d.ts +26 -0
  92. package/lib/client/local.js +190 -0
  93. package/lib/client/object-url.d.ts +8 -0
  94. package/lib/client/object-url.js +21 -0
  95. package/lib/client/types.d.ts +268 -0
  96. package/lib/client/types.js +8 -0
  97. package/lib/commands/agent-tools.d.ts +31 -0
  98. package/lib/commands/agent-tools.js +100 -0
  99. package/lib/commands/index.d.ts +56 -0
  100. package/lib/commands/index.js +298 -0
  101. package/lib/commands/parse.d.ts +73 -0
  102. package/lib/commands/parse.js +231 -0
  103. package/lib/commands/present.d.ts +134 -0
  104. package/lib/commands/present.js +198 -0
  105. package/lib/commands/session.d.ts +85 -0
  106. package/lib/commands/session.js +138 -0
  107. package/lib/commands/verbs.d.ts +35 -0
  108. package/lib/commands/verbs.js +842 -0
  109. package/lib/core/actors.d.ts +18 -0
  110. package/lib/core/actors.js +45 -0
  111. package/lib/core/agent.d.ts +128 -0
  112. package/lib/core/agent.js +323 -0
  113. package/lib/core/boards.d.ts +113 -0
  114. package/lib/core/boards.js +288 -0
  115. package/lib/core/bootstrap.d.ts +47 -0
  116. package/lib/core/bootstrap.js +100 -0
  117. package/lib/core/bytes.d.ts +10 -0
  118. package/lib/core/bytes.js +52 -0
  119. package/lib/core/card-paths.d.ts +25 -0
  120. package/lib/core/card-paths.js +44 -0
  121. package/lib/core/context.d.ts +60 -0
  122. package/lib/core/context.js +42 -0
  123. package/lib/core/cross-board.d.ts +81 -0
  124. package/lib/core/cross-board.js +228 -0
  125. package/lib/core/describe.d.ts +21 -0
  126. package/lib/core/describe.js +81 -0
  127. package/lib/core/errors.d.ts +159 -0
  128. package/lib/core/errors.js +160 -0
  129. package/lib/core/files.d.ts +79 -0
  130. package/lib/core/files.js +262 -0
  131. package/lib/core/history.d.ts +20 -0
  132. package/lib/core/history.js +53 -0
  133. package/lib/core/image-types.d.ts +16 -0
  134. package/lib/core/image-types.js +36 -0
  135. package/lib/core/model.d.ts +263 -0
  136. package/lib/core/model.js +34 -0
  137. package/lib/core/nebula-link.d.ts +14 -0
  138. package/lib/core/nebula-link.js +39 -0
  139. package/lib/core/relations.d.ts +58 -0
  140. package/lib/core/relations.js +112 -0
  141. package/lib/core/search.d.ts +38 -0
  142. package/lib/core/search.js +92 -0
  143. package/lib/core/sessions.d.ts +44 -0
  144. package/lib/core/sessions.js +158 -0
  145. package/lib/core/snapshot-schema.d.ts +295 -0
  146. package/lib/core/snapshot-schema.js +82 -0
  147. package/lib/core/snapshots.d.ts +75 -0
  148. package/lib/core/snapshots.js +409 -0
  149. package/lib/core/states.d.ts +25 -0
  150. package/lib/core/states.js +126 -0
  151. package/lib/core/text.d.ts +40 -0
  152. package/lib/core/text.js +113 -0
  153. package/lib/core/tickets.d.ts +69 -0
  154. package/lib/core/tickets.js +311 -0
  155. package/lib/core/trello.d.ts +17 -0
  156. package/lib/core/trello.js +269 -0
  157. package/lib/core/verdicts.d.ts +28 -0
  158. package/lib/core/verdicts.js +92 -0
  159. package/lib/core/views.d.ts +95 -0
  160. package/lib/core/views.js +186 -0
  161. package/lib/http/app.d.ts +46 -0
  162. package/lib/http/app.js +491 -0
  163. package/lib/mcp-server.d.ts +10 -0
  164. package/lib/mcp-server.js +120 -0
  165. package/lib/node/assets.d.ts +7 -0
  166. package/lib/node/assets.js +68 -0
  167. package/lib/node/data-folder.d.ts +21 -0
  168. package/lib/node/data-folder.js +34 -0
  169. package/lib/node/env.d.ts +38 -0
  170. package/lib/node/env.js +49 -0
  171. package/lib/node/host-check.d.ts +24 -0
  172. package/lib/node/host-check.js +53 -0
  173. package/lib/node/host-services.d.ts +9 -0
  174. package/lib/node/host-services.js +150 -0
  175. package/lib/node/local.d.ts +22 -0
  176. package/lib/node/local.js +61 -0
  177. package/lib/node/migrate.d.ts +53 -0
  178. package/lib/node/migrate.js +635 -0
  179. package/lib/node/package.d.ts +7 -0
  180. package/lib/node/package.js +14 -0
  181. package/lib/node/repo-board.d.ts +49 -0
  182. package/lib/node/repo-board.js +245 -0
  183. package/lib/node/serve.d.ts +8 -0
  184. package/lib/node/serve.js +106 -0
  185. package/lib/orbit.d.ts +47 -0
  186. package/lib/orbit.js +44 -0
  187. package/lib/quiet-sqlite-warning.d.ts +1 -0
  188. package/lib/quiet-sqlite-warning.js +18 -0
  189. package/lib/store/blob-path.d.ts +6 -0
  190. package/lib/store/blob-path.js +12 -0
  191. package/lib/store/conformance.d.ts +52 -0
  192. package/lib/store/conformance.js +1449 -0
  193. package/lib/store/search-words.d.ts +10 -0
  194. package/lib/store/search-words.js +19 -0
  195. package/lib/store/types.d.ts +452 -0
  196. package/lib/store/types.js +34 -0
  197. package/llms.txt +63 -0
  198. package/package.json +109 -25
  199. package/src/cli/orbit.js +11 -658
  200. package/src/mcp-server.js +16 -876
  201. package/SKILL-ORBIT.md +0 -151
  202. package/build.mjs +0 -210
  203. package/dist/full/js/app.4e6fbe20.js +0 -952
  204. package/dist/full/js/app.4e6fbe20.js.map +0 -7
  205. package/dist/full/styles.4e60d824.css +0 -1
  206. package/docs/AGENT_PROTOCOL.md +0 -424
  207. package/docs/MCP_SETUP.md +0 -54
  208. package/docs/ORBIT_DISPATCH.md +0 -280
  209. package/docs/SS-Orbit-Dark.png +0 -0
  210. package/docs/SS-Orbit-Light.png +0 -0
  211. package/docs/mcp-client.example.json +0 -13
  212. package/docs/setup/orbit-init-wizard.md +0 -174
  213. package/public/apple-touch-icon.png +0 -0
  214. package/public/cursors/live_arrow.cur +0 -0
  215. package/public/cursors/live_link.cur +0 -0
  216. package/public/cursors/live_move.cur +0 -0
  217. package/public/cursors/live_select.cur +0 -0
  218. package/public/cursors/live_unavail.cur +0 -0
  219. package/public/favicon-32x32.png +0 -0
  220. package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
  221. package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
  222. package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
  223. package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
  224. package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
  225. package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
  226. package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
  227. package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
  228. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
  229. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
  230. package/public/fonts/Chakra_Petch/OFL.txt +0 -93
  231. package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
  232. package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
  233. package/public/fonts/Inter/OFL.txt +0 -93
  234. package/public/fonts/Inter/README.txt +0 -118
  235. package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
  236. package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
  237. package/public/fonts/JetBrains_Mono/README.txt +0 -79
  238. package/public/fonts/OpenRing-Regular.ttf +0 -0
  239. package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  240. package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
  241. package/public/fonts/Open_Sans/README.txt +0 -100
  242. package/public/fonts/Orbitron/OFL.txt +0 -93
  243. package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
  244. package/public/fonts/Orbitron/README.txt +0 -68
  245. package/public/fonts/Rajdhani/OFL.txt +0 -93
  246. package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
  247. package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
  248. package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
  249. package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
  250. package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
  251. package/public/fonts/runescape-uf.ttf +0 -0
  252. package/public/index.html +0 -769
  253. package/public/js/actor-labels.js +0 -65
  254. package/public/js/api.js +0 -39
  255. package/public/js/app.js +0 -45
  256. package/public/js/artifact-drawer.js +0 -79
  257. package/public/js/board-export.js +0 -161
  258. package/public/js/board-menu.js +0 -374
  259. package/public/js/card-actions.js +0 -545
  260. package/public/js/config.js +0 -20
  261. package/public/js/create-card.js +0 -190
  262. package/public/js/dom.js +0 -19
  263. package/public/js/drawer.js +0 -273
  264. package/public/js/font-preference.js +0 -230
  265. package/public/js/format.js +0 -433
  266. package/public/js/help-menu.js +0 -31
  267. package/public/js/icons.js +0 -16
  268. package/public/js/kanban.js +0 -694
  269. package/public/js/local-backend.js +0 -1015
  270. package/public/js/main.js +0 -161
  271. package/public/js/modal.js +0 -33
  272. package/public/js/motion-preference.js +0 -64
  273. package/public/js/panel.js +0 -58
  274. package/public/js/router.js +0 -134
  275. package/public/js/search.js +0 -56
  276. package/public/js/settings-tabs.js +0 -7
  277. package/public/js/settings.js +0 -1634
  278. package/public/js/sse.js +0 -159
  279. package/public/js/starscape.js +0 -332
  280. package/public/js/state.js +0 -129
  281. package/public/js/text-editing.js +0 -128
  282. package/public/js/theme-preference.js +0 -95
  283. package/public/js/ticket-detail.js +0 -1516
  284. package/public/js/ticket-typeahead.js +0 -49
  285. package/public/js/toast.js +0 -89
  286. package/public/js/topbar-notes.js +0 -155
  287. package/public/js/transport.js +0 -22
  288. package/public/js/typeahead.js +0 -206
  289. package/public/js/unread.js +0 -121
  290. package/public/js/update-check.js +0 -58
  291. package/public/js/url-routes.js +0 -94
  292. package/public/orbits-favicon.png +0 -0
  293. package/public/orbits-logo-bg-wide.png +0 -0
  294. package/public/orbits-logo-bg.png +0 -0
  295. package/public/orbits-logo.png +0 -0
  296. package/public/styles.css +0 -7709
  297. package/src/core/agent.js +0 -387
  298. package/src/core/agents-md.js +0 -109
  299. package/src/core/artifacts.js +0 -149
  300. package/src/core/attachments.js +0 -238
  301. package/src/core/auth.js +0 -85
  302. package/src/core/backups.js +0 -142
  303. package/src/core/board.js +0 -148
  304. package/src/core/boards.js +0 -409
  305. package/src/core/bootstrap.js +0 -170
  306. package/src/core/css-bundle.js +0 -30
  307. package/src/core/db.js +0 -315
  308. package/src/core/delete-board.js +0 -163
  309. package/src/core/dispatch.js +0 -1007
  310. package/src/core/events.js +0 -314
  311. package/src/core/http.js +0 -115
  312. package/src/core/migrate.js +0 -198
  313. package/src/core/paths.js +0 -55
  314. package/src/core/provision-repo-board.js +0 -119
  315. package/src/core/queries.js +0 -246
  316. package/src/core/registry.js +0 -211
  317. package/src/core/relations.js +0 -163
  318. package/src/core/review-verdicts.js +0 -138
  319. package/src/core/router.js +0 -556
  320. package/src/core/search.js +0 -183
  321. package/src/core/seed.js +0 -234
  322. package/src/core/snapshots.js +0 -692
  323. package/src/core/states.js +0 -167
  324. package/src/core/system-picker.js +0 -109
  325. package/src/core/tickets.js +0 -400
  326. package/src/core/util.js +0 -106
  327. package/src/core/version-check.js +0 -154
  328. package/src/mcp/orbit-client.js +0 -287
  329. package/src/server.js +0 -115
@@ -1,692 +0,0 @@
1
- import { resetBoard, tx, getRegistry } from "./db.js";
2
- import { backupBoardDatabase } from "./backups.js";
3
- import { boardById, labelsForTicket } from "./queries.js";
4
- import { requireBoardAccess } from "./auth.js";
5
- import { recordEvent } from "./events.js";
6
- import { computeNewBoardDbPath, ensureBoardDbFileAndSchema, syncRegistryFromBoardDb } from "./registry.js";
7
- import {
8
- httpError,
9
- id,
10
- normalizePriority,
11
- normalizeProjectEntryType,
12
- normalizeTicketType,
13
- now,
14
- normalizePath,
15
- requiredString,
16
- slugify
17
- } from "./util.js";
18
- import { reindexAllTickets } from "./tickets.js";
19
- import {
20
- attachmentRowsForBoard,
21
- importAttachmentRows,
22
- serializeAttachmentForExport
23
- } from "./attachments.js";
24
-
25
- export function exportBoard(boardId, ctx, options = {}) {
26
- const { db, board, actor } = ctx;
27
- if (boardId !== board.id) throw httpError(404, "board_not_found");
28
- const innerBoard = boardById(db, boardId);
29
- if (!innerBoard) throw httpError(404, "board_not_found");
30
- requireBoardAccess(actor, innerBoard);
31
-
32
- const states = db.prepare("SELECT * FROM states WHERE board_id = ? ORDER BY position").all(boardId);
33
- const tickets = db
34
- .prepare("SELECT * FROM tickets WHERE board_id = ? ORDER BY number ASC")
35
- .all(boardId)
36
- .map((ticket) => ({ ...ticket, labels: labelsForTicket(db, ticket.id) }));
37
- const ticketIds = tickets.map((ticket) => ticket.id);
38
- const comments = ticketIds.length
39
- ? db
40
- .prepare("SELECT * FROM comments WHERE ticket_id IN (SELECT id FROM tickets WHERE board_id = ?) ORDER BY created_at ASC")
41
- .all(boardId)
42
- : [];
43
- const reviewVerdicts = ticketIds.length
44
- ? db
45
- .prepare("SELECT * FROM review_verdicts WHERE ticket_id IN (SELECT id FROM tickets WHERE board_id = ?) ORDER BY created_at ASC")
46
- .all(boardId)
47
- : [];
48
- const relations = ticketIds.length
49
- ? db
50
- .prepare(
51
- `SELECT *
52
- FROM relations
53
- WHERE source_ticket_id IN (SELECT id FROM tickets WHERE board_id = ?)
54
- OR target_ticket_id IN (SELECT id FROM tickets WHERE board_id = ?)
55
- ORDER BY created_at ASC`
56
- )
57
- .all(boardId, boardId)
58
- : [];
59
- const entries = db
60
- .prepare("SELECT * FROM board_entries WHERE board_id = ? ORDER BY created_at ASC")
61
- .all(boardId);
62
- const labels = db.prepare("SELECT * FROM labels WHERE board_id = ? ORDER BY name ASC").all(boardId);
63
- const events = db
64
- .prepare(
65
- `SELECT *
66
- FROM events
67
- WHERE ticket_id IS NULL
68
- OR ticket_id IN (SELECT id FROM tickets WHERE board_id = ?)
69
- ORDER BY created_at ASC`
70
- )
71
- .all(boardId);
72
- const attachments = attachmentRowsForBoard(db, boardId).map((row) =>
73
- serializeAttachmentForExport(board, row, Boolean(options.includeAttachments))
74
- );
75
-
76
- return {
77
- format: "orbit-board-export",
78
- version: 9,
79
- exported_at: now(),
80
- include_attachments: Boolean(options.includeAttachments),
81
- board: { ...innerBoard },
82
- states,
83
- labels,
84
- tickets,
85
- comments,
86
- review_verdicts: reviewVerdicts,
87
- relations,
88
- attachments,
89
- board_entries: entries,
90
- events
91
- };
92
- }
93
-
94
- export function importBoardSnapshot(body, ctx) {
95
- const { db, actor, board } = ctx;
96
- const snapshot = normalizeImportSnapshot(body?.snapshot || body, ctx);
97
- const format = snapshot?.format;
98
- if (!snapshot || format !== "orbit-board-export") throw httpError(400, "invalid_import_snapshot");
99
- if (!snapshot.board) throw httpError(400, "snapshot_missing_board");
100
-
101
- const replaceExisting = Boolean(body?.replace_existing);
102
- const existingCount = db.prepare("SELECT COUNT(*) AS count FROM boards").get().count;
103
- if (existingCount > 0 && !replaceExisting) {
104
- throw httpError(409, "import_requires_empty_board_or_replace_existing");
105
- }
106
-
107
- // Snapshot the existing DB file before a destructive replace, outside the
108
- // transaction (it copies the file on disk, not rows).
109
- if (existingCount > 0 && replaceExisting) {
110
- backupBoardDatabase(ctx.board, db, "pre-import-replace");
111
- }
112
-
113
- const targetBoardId = board.id;
114
- const importedBoard = {
115
- ...snapshot.board,
116
- id: targetBoardId
117
- };
118
-
119
- const result = tx(db, () => {
120
- if (existingCount > 0 && replaceExisting) resetBoard(db);
121
-
122
- db.prepare(
123
- `INSERT INTO boards
124
- (id, slug, name, repo_url, system_path, default_branch,
125
- project_notes, agent_instructions, ai_enabled, created_at, updated_at)
126
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
127
- ).run(
128
- importedBoard.id,
129
- importedBoard.slug,
130
- importedBoard.name,
131
- importedBoard.repo_url || "",
132
- importedBoard.system_path || "",
133
- importedBoard.default_branch || "main",
134
- importedBoard.project_notes || "",
135
- importedBoard.agent_instructions || "",
136
- importedBoard.ai_enabled === 0 ? 0 : 1,
137
- importedBoard.created_at || now(),
138
- importedBoard.updated_at || now()
139
- );
140
-
141
- for (const state of snapshot.states || []) {
142
- db.prepare(
143
- `INSERT INTO states
144
- (id, board_id, name, position, is_default, role, created_at)
145
- VALUES (?, ?, ?, ?, ?, ?, ?)`
146
- ).run(
147
- state.id,
148
- targetBoardId,
149
- state.name,
150
- state.position,
151
- state.is_default || 0,
152
- state.role || null,
153
- state.created_at || now()
154
- );
155
- }
156
-
157
- for (const label of snapshot.labels || []) {
158
- db.prepare("INSERT INTO labels (id, board_id, name, color, created_at) VALUES (?, ?, ?, ?, ?)").run(
159
- label.id,
160
- targetBoardId,
161
- label.name,
162
- label.color || "#64748b",
163
- label.created_at || now()
164
- );
165
- }
166
-
167
- for (const ticket of snapshot.tickets || []) {
168
- db.prepare(
169
- `INSERT INTO tickets
170
- (id, board_id, number, title, description, type, parent_ticket_id, ai_plan,
171
- plan_artifact_path, implementation_summary, implementation_artifact_path,
172
- implementation_updates, state_id, priority,
173
- created_by, created_at, updated_at, archived_at)
174
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
175
- ).run(
176
- ticket.id,
177
- targetBoardId,
178
- ticket.number,
179
- ticket.title,
180
- ticket.description || "",
181
- normalizeTicketType(ticket.type || "task"),
182
- ticket.parent_ticket_id || null,
183
- ticket.ai_plan || "",
184
- ticket.plan_artifact_path || "",
185
- ticket.implementation_summary || "",
186
- ticket.implementation_artifact_path || "",
187
- ticket.implementation_updates || "",
188
- ticket.state_id,
189
- normalizePriority(ticket.priority),
190
- ticket.created_by || "import",
191
- ticket.created_at || now(),
192
- ticket.updated_at || now(),
193
- ticket.archived_at || null
194
- );
195
-
196
- for (const label of ticket.labels || []) {
197
- db.prepare("INSERT OR IGNORE INTO ticket_labels (ticket_id, label_id) VALUES (?, ?)").run(ticket.id, label.id);
198
- }
199
- }
200
-
201
- for (const comment of snapshot.comments || []) {
202
- db.prepare(
203
- "INSERT INTO comments (id, ticket_id, author, kind, body, omitted_for_ai, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
204
- ).run(
205
- comment.id,
206
- comment.ticket_id,
207
- comment.author || "import",
208
- comment.kind || "comment",
209
- comment.body || "",
210
- comment.omitted_for_ai ? 1 : 0,
211
- comment.created_at || now()
212
- );
213
- }
214
-
215
- for (const review of snapshot.review_verdicts || []) {
216
- db.prepare(
217
- `INSERT INTO review_verdicts
218
- (id, ticket_id, verdict, blocking_findings_json, optional_findings_json,
219
- evidence_commands_json, reviewer_profile, reviewer_session_id,
220
- reviewed_commit_sha, dispatch_run_id, supersedes_prior_review_id,
221
- created_by, created_at)
222
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
223
- ).run(
224
- review.id,
225
- review.ticket_id,
226
- review.verdict,
227
- review.blocking_findings_json || "[]",
228
- review.optional_findings_json || "[]",
229
- review.evidence_commands_json || "[]",
230
- review.reviewer_profile || "",
231
- review.reviewer_session_id || "",
232
- review.reviewed_commit_sha || "",
233
- review.dispatch_run_id || "",
234
- review.supersedes_prior_review_id || null,
235
- review.created_by || "import",
236
- review.created_at || now()
237
- );
238
- }
239
-
240
- for (const relation of snapshot.relations || []) {
241
- db.prepare(
242
- "INSERT OR IGNORE INTO relations (id, source_ticket_id, target_ticket_id, type, created_at) VALUES (?, ?, ?, ?, ?)"
243
- ).run(
244
- relation.id,
245
- relation.source_ticket_id,
246
- relation.target_ticket_id,
247
- relation.type || "relates_to",
248
- relation.created_at || now()
249
- );
250
- }
251
-
252
- importAttachmentRows(snapshot, ctx, targetBoardId);
253
-
254
- for (const entry of snapshot.board_entries || []) {
255
- db.prepare(
256
- `INSERT INTO board_entries
257
- (id, board_id, type, title, body, ticket_id, created_by, created_at, updated_at, struck_at)
258
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
259
- ).run(
260
- entry.id,
261
- targetBoardId,
262
- normalizeProjectEntryType(entry.type),
263
- requiredString(entry.title, "entry_title"),
264
- entry.body || "",
265
- entry.ticket_id || null,
266
- entry.created_by || "import",
267
- entry.created_at || now(),
268
- entry.updated_at || now(),
269
- entry.struck_at || null
270
- );
271
- }
272
-
273
- for (const event of snapshot.events || []) {
274
- db.prepare(
275
- "INSERT INTO events (id, ticket_id, actor, type, body_json, created_at) VALUES (?, ?, ?, ?, ?, ?)"
276
- ).run(
277
- event.id,
278
- event.ticket_id || null,
279
- event.actor || "import",
280
- event.type || "imported_event",
281
- event.body_json || "{}",
282
- event.created_at || now()
283
- );
284
- }
285
-
286
- // Reindex inside the transaction: a committed import must never leave the
287
- // FTS table empty/stale (previously this ran after COMMIT and a throw here
288
- // left search broken on otherwise-imported data).
289
- reindexAllTickets(db);
290
- recordEvent(db, targetBoardId, "board_imported", null, actor.name, {
291
- board_id: targetBoardId,
292
- replace_existing: replaceExisting
293
- });
294
- return {
295
- ok: true,
296
- imported_board_id: targetBoardId,
297
- imported_board_slug: importedBoard.slug,
298
- replace_existing: replaceExisting,
299
- source_format: snapshot.source_format || "orbit-board-export",
300
- imported_counts: snapshot.imported_counts || {
301
- states: (snapshot.states || []).length,
302
- tickets: (snapshot.tickets || []).length,
303
- comments: (snapshot.comments || []).length,
304
- labels: (snapshot.labels || []).length
305
- }
306
- };
307
- });
308
-
309
- // Registry sync writes the separate registry DB (a different connection), so
310
- // it runs after the board-DB transaction commits, not inside it.
311
- syncRegistryFromBoardDb(board, db);
312
- return result;
313
- }
314
-
315
- export function importBoardSnapshotAsNewBoard(body, ctx) {
316
- const rawSnapshot = body?.snapshot || body;
317
- const actor = ctx.actor;
318
- const sourceBoard = ctx.board || {};
319
- const repoPath = normalizePath(body?.repo_path || body?.system_path || sourceBoard.repo_path || sourceBoard.system_path || process.cwd());
320
- const sourceFormatSnapshot = normalizeImportSnapshot(rawSnapshot, {
321
- ...ctx,
322
- board: {
323
- id: id(),
324
- slug: "",
325
- name: "",
326
- repo_path: repoPath,
327
- system_path: repoPath,
328
- repo_url: sourceBoard.repo_url || "",
329
- default_branch: sourceBoard.default_branch || "main"
330
- }
331
- });
332
- const format = sourceFormatSnapshot?.format;
333
- if (!sourceFormatSnapshot || format !== "orbit-board-export") throw httpError(400, "invalid_import_snapshot");
334
- if (!sourceFormatSnapshot.board) throw httpError(400, "snapshot_missing_board");
335
-
336
- const boardName = cleanText(sourceFormatSnapshot.board.name) || cleanText(rawSnapshot?.name) || "Imported Board";
337
- const boardSlug = uniqueImportedBoardSlug(body?.slug || sourceFormatSnapshot.board.slug || rawSnapshot?.name || boardName);
338
- const boardId = id();
339
- const dbPath = computeNewBoardDbPath(repoPath, boardSlug);
340
- const db = ensureBoardDbFileAndSchema(dbPath);
341
- const newBoardCtx = {
342
- actor,
343
- board: {
344
- id: boardId,
345
- slug: boardSlug,
346
- name: boardName,
347
- repo_path: repoPath,
348
- db_path: dbPath,
349
- system_path: repoPath,
350
- repo_url: sourceBoard.repo_url || sourceFormatSnapshot.board.repo_url || "",
351
- default_branch: sourceBoard.default_branch || sourceFormatSnapshot.board.default_branch || "main"
352
- },
353
- db
354
- };
355
- const normalizedSnapshot = normalizeImportSnapshot(rawSnapshot, newBoardCtx);
356
- const snapshotForNewBoard = {
357
- ...normalizedSnapshot,
358
- board: {
359
- ...normalizedSnapshot.board,
360
- id: boardId,
361
- slug: boardSlug,
362
- name: cleanText(normalizedSnapshot.board?.name) || boardName,
363
- repo_url: normalizedSnapshot.board?.repo_url || sourceBoard.repo_url || "",
364
- system_path: normalizedSnapshot.board?.system_path || repoPath,
365
- default_branch: normalizedSnapshot.board?.default_branch || sourceBoard.default_branch || "main"
366
- }
367
- };
368
-
369
- return {
370
- ...importBoardSnapshot({ snapshot: snapshotForNewBoard, replace_existing: false }, newBoardCtx),
371
- created_new_board: true
372
- };
373
- }
374
-
375
- function uniqueImportedBoardSlug(value) {
376
- const base = slugify(value) || `imported-board-${id().slice(0, 8)}`;
377
- const reg = getRegistry();
378
- if (!reg.prepare("SELECT id FROM boards WHERE slug = ?").get(base)) return base;
379
- for (let i = 2; i < 1000; i += 1) {
380
- const candidate = `${base}-${i}`;
381
- if (!reg.prepare("SELECT id FROM boards WHERE slug = ?").get(candidate)) return candidate;
382
- }
383
- return `${base}-${id().slice(0, 8)}`;
384
- }
385
-
386
- function normalizeImportSnapshot(snapshot, ctx) {
387
- if (snapshot?.format === "orbit-board-export") return snapshot;
388
- if (looksLikeTrelloExport(snapshot)) return trelloExportToOrbitSnapshot(snapshot, ctx);
389
- return snapshot;
390
- }
391
-
392
- function looksLikeTrelloExport(value) {
393
- return Boolean(
394
- value &&
395
- typeof value === "object" &&
396
- Array.isArray(value.cards) &&
397
- Array.isArray(value.lists) &&
398
- (typeof value.id === "string" || typeof value.name === "string")
399
- );
400
- }
401
-
402
- const TRELLO_LABEL_COLORS = {
403
- green: "#61bd4f",
404
- yellow: "#f2d600",
405
- orange: "#ff9f1a",
406
- red: "#eb5a46",
407
- purple: "#c377e0",
408
- blue: "#0079bf",
409
- sky: "#00c2e0",
410
- lime: "#51e898",
411
- pink: "#ff78cb",
412
- black: "#344563"
413
- };
414
-
415
- function trelloExportToOrbitSnapshot(trello, ctx) {
416
- const time = now();
417
- const targetBoard = ctx.board || {};
418
- const boardName = cleanText(trello.name) || "Imported Trello Board";
419
- const boardSlug = targetBoard.slug || slugify(boardName) || `trello-${id().slice(0, 8)}`;
420
- const labelsById = buildTrelloLabelMap(trello, time);
421
- const checklistsByCard = groupTrelloChecklists(trello);
422
- const commentsByCard = groupTrelloComments(trello);
423
- const sortedLists = [...(trello.lists || [])].sort(compareTrelloPosition);
424
- const states = buildTrelloStates(sortedLists, time);
425
- const fallbackStateId = states[0]?.id;
426
- const stateIdsByTrelloListId = new Map(sortedLists.map((list, index) => [list.id, states[index]?.id || fallbackStateId]));
427
- const sortedCards = [...(trello.cards || [])].sort(compareTrelloPosition);
428
- const tickets = [];
429
- const comments = [];
430
-
431
- sortedCards.forEach((card, index) => {
432
- const ticketId = trelloScopedId("card", card.id || `${index + 1}`);
433
- const stateId = stateIdsByTrelloListId.get(card.idList) || fallbackStateId;
434
- if (!stateId) return;
435
- const cardUpdatedAt = cleanText(card.dateLastActivity) || time;
436
- const cardCreatedAt = dateFromTrelloObjectId(card.id) || cardUpdatedAt;
437
- const cardComments = commentsByCard.get(card.id) || [];
438
- const ticketLabels = trelloLabelsForCard(card, labelsById);
439
- const importedDescription = renderTrelloCardDescription(card, {
440
- listName: listNameForCard(trello.lists, card.idList),
441
- checklists: checklistsByCard.get(card.id) || []
442
- });
443
- tickets.push({
444
- id: ticketId,
445
- number: index + 1,
446
- title: cleanText(card.name) || `Untitled Trello card ${index + 1}`,
447
- description: importedDescription,
448
- type: "task",
449
- parent_ticket_id: null,
450
- ai_plan: "",
451
- implementation_summary: "",
452
- implementation_updates: "",
453
- state_id: stateId,
454
- priority: priorityFromTrelloCard(card),
455
- created_by: "trello-import",
456
- created_at: cardCreatedAt,
457
- updated_at: cardUpdatedAt,
458
- archived_at: card.closed ? cardUpdatedAt : null,
459
- labels: ticketLabels
460
- });
461
-
462
- cardComments.forEach((action, commentIndex) => {
463
- const commentId = trelloScopedId("comment", action.id || `${card.id}-${commentIndex + 1}`);
464
- comments.push({
465
- id: commentId,
466
- ticket_id: ticketId,
467
- author: trelloMemberName(action.memberCreator),
468
- kind: "human_comment",
469
- body: cleanText(action.data?.text) || "",
470
- omitted_for_ai: 0,
471
- created_at: cleanText(action.date) || cardUpdatedAt
472
- });
473
- });
474
- });
475
-
476
- return {
477
- format: "orbit-board-export",
478
- source_format: "trello-board-export",
479
- version: 9,
480
- exported_at: time,
481
- include_attachments: false,
482
- imported_counts: {
483
- states: states.length,
484
- tickets: tickets.length,
485
- comments: comments.length,
486
- labels: labelsById.size
487
- },
488
- board: {
489
- id: targetBoard.id || trelloScopedId("board", trello.id || id()),
490
- slug: boardSlug,
491
- name: boardName,
492
- repo_url: targetBoard.repo_url || "",
493
- system_path: targetBoard.system_path || targetBoard.repo_path || "",
494
- default_branch: targetBoard.default_branch || "main",
495
- project_notes: cleanText(trello.desc) ? `Imported from Trello.\n\n${cleanText(trello.desc)}` : "Imported from Trello.",
496
- agent_instructions: "",
497
- ai_enabled: 1,
498
- created_at: cleanText(trello.dateLastActivity) || time,
499
- updated_at: time
500
- },
501
- states,
502
- labels: [...labelsById.values()],
503
- tickets,
504
- comments: comments.filter((comment) => comment.body),
505
- review_verdicts: [],
506
- relations: [],
507
- attachments: [],
508
- board_entries: [],
509
- events: []
510
- };
511
- }
512
-
513
- function buildTrelloStates(lists = [], time) {
514
- const sorted = [...lists].sort(compareTrelloPosition);
515
- const rolesSeen = new Set();
516
- const firstOpenIndex = sorted.findIndex((list) => !list.closed);
517
- const defaultIndex = firstOpenIndex >= 0 ? firstOpenIndex : 0;
518
- return sorted.map((list, index) => {
519
- const role = uniqueTrelloStateRole(inferTrelloStateRole(list.name), rolesSeen);
520
- return {
521
- id: trelloScopedId("list", list.id || `${index + 1}`),
522
- name: cleanText(list.name) || `Trello List ${index + 1}`,
523
- position: index,
524
- is_default: index === defaultIndex ? 1 : 0,
525
- role,
526
- created_at: dateFromTrelloObjectId(list.id) || time
527
- };
528
- });
529
- }
530
-
531
- function buildTrelloLabelMap(trello, time) {
532
- const labels = new Map();
533
- const addLabel = (raw) => {
534
- if (!raw) return;
535
- const key = raw.id || `${raw.color || "label"}-${raw.name || ""}`;
536
- const name = trelloLabelName(raw, trello.labelNames);
537
- if (!name || labels.has(key)) return;
538
- labels.set(key, {
539
- id: trelloScopedId("label", key),
540
- name,
541
- color: TRELLO_LABEL_COLORS[String(raw.color || "").toLowerCase()] || "#64748b",
542
- created_at: dateFromTrelloObjectId(raw.id) || time
543
- });
544
- };
545
- for (const label of trello.labels || []) addLabel(label);
546
- for (const card of trello.cards || []) for (const label of card.labels || []) addLabel(label);
547
- return labels;
548
- }
549
-
550
- function groupTrelloChecklists(trello) {
551
- const grouped = new Map();
552
- const add = (cardId, checklist) => {
553
- if (!cardId || !checklist) return;
554
- if (!grouped.has(cardId)) grouped.set(cardId, []);
555
- grouped.get(cardId).push(checklist);
556
- };
557
- for (const checklist of trello.checklists || []) add(checklist.idCard, checklist);
558
- for (const card of trello.cards || []) for (const checklist of card.checklists || []) add(card.id, checklist);
559
- return grouped;
560
- }
561
-
562
- function groupTrelloComments(trello) {
563
- const grouped = new Map();
564
- for (const action of trello.actions || []) {
565
- if (action.type !== "commentCard") continue;
566
- const cardId = action.data?.card?.id;
567
- if (!cardId) continue;
568
- if (!grouped.has(cardId)) grouped.set(cardId, []);
569
- grouped.get(cardId).push(action);
570
- }
571
- for (const actions of grouped.values()) actions.sort((a, b) => String(a.date || "").localeCompare(String(b.date || "")));
572
- return grouped;
573
- }
574
-
575
- function trelloLabelsForCard(card, labelsById) {
576
- const result = [];
577
- const seen = new Set();
578
- const addByRawId = (rawId) => {
579
- const label = labelsById.get(rawId);
580
- if (label && !seen.has(label.id)) {
581
- seen.add(label.id);
582
- result.push(label);
583
- }
584
- };
585
- for (const label of card.labels || []) addByRawId(label.id || `${label.color || "label"}-${label.name || ""}`);
586
- for (const labelId of card.idLabels || []) addByRawId(labelId);
587
- return result;
588
- }
589
-
590
- function renderTrelloCardDescription(card, { listName, checklists }) {
591
- const parts = [];
592
- const desc = cleanText(card.desc);
593
- if (desc) parts.push(desc);
594
- const meta = [];
595
- if (card.idShort) meta.push(`Trello card #${card.idShort}`);
596
- if (listName) meta.push(`List: ${listName}`);
597
- if (card.shortUrl || card.url) meta.push(`URL: ${card.shortUrl || card.url}`);
598
- if (card.due) meta.push(`Due: ${card.due}${card.dueComplete ? " (complete)" : ""}`);
599
- if (card.closed) meta.push("Archived in Trello: yes");
600
- if (meta.length) parts.push(["Imported from Trello:", ...meta.map((line) => `- ${line}`)].join("\n"));
601
-
602
- const checklistText = renderTrelloChecklists(checklists);
603
- if (checklistText) parts.push(checklistText);
604
- const attachmentText = renderTrelloAttachments(card.attachments || []);
605
- if (attachmentText) parts.push(attachmentText);
606
- return parts.join("\n\n");
607
- }
608
-
609
- function renderTrelloChecklists(checklists = []) {
610
- const rendered = checklists
611
- .map((checklist) => {
612
- const items = [...(checklist.checkItems || [])].sort(compareTrelloPosition);
613
- if (!items.length) return "";
614
- return [`Checklist: ${cleanText(checklist.name) || "Untitled"}`, ...items.map((item) => `- [${item.state === "complete" ? "x" : " "}] ${cleanText(item.name) || "Untitled item"}`)].join("\n");
615
- })
616
- .filter(Boolean);
617
- return rendered.join("\n\n");
618
- }
619
-
620
- function renderTrelloAttachments(attachments = []) {
621
- const rendered = attachments
622
- .map((attachment) => {
623
- const name = cleanText(attachment.name) || cleanText(attachment.fileName) || "Attachment";
624
- const url = cleanText(attachment.url);
625
- return url ? `- ${name}: ${url}` : `- ${name}`;
626
- })
627
- .filter(Boolean);
628
- return rendered.length ? ["Trello attachments:", ...rendered].join("\n") : "";
629
- }
630
-
631
- function trelloLabelName(label, labelNames = {}) {
632
- const explicit = cleanText(label.name);
633
- if (explicit) return explicit;
634
- const color = cleanText(label.color);
635
- const namedColor = color ? cleanText(labelNames[color]) : "";
636
- if (namedColor) return namedColor;
637
- return color ? `Trello ${color}` : "";
638
- }
639
-
640
- function listNameForCard(lists = [], idList) {
641
- return cleanText(lists.find((list) => list.id === idList)?.name);
642
- }
643
-
644
- function priorityFromTrelloCard(card) {
645
- const names = (card.labels || []).map((label) => cleanText(label.name || label.color).toLowerCase()).join(" ");
646
- if (/urgent|critical|blocker|high|red/.test(names)) return 4;
647
- if (/low|someday|backlog/.test(names)) return 1;
648
- return 2;
649
- }
650
-
651
- function inferTrelloStateRole(name) {
652
- const normalized = cleanText(name).toLowerCase();
653
- if (!normalized) return null;
654
- if (/^ai ready$|ai[- ]ready/.test(normalized)) return "ai_ready";
655
- if (/review|qa|verify|validation/.test(normalized)) return "review";
656
- if (/doing|in progress|active|working|wip/.test(normalized)) return "in_progress";
657
- if (/done|complete|completed|shipped|closed/.test(normalized)) return "done";
658
- return null;
659
- }
660
-
661
- function uniqueTrelloStateRole(role, rolesSeen) {
662
- if (!role || rolesSeen.has(role)) return null;
663
- rolesSeen.add(role);
664
- return role;
665
- }
666
-
667
- function trelloMemberName(member) {
668
- return cleanText(member?.fullName) || cleanText(member?.username) || "Trello";
669
- }
670
-
671
- function compareTrelloPosition(a, b) {
672
- const pa = Number(a?.pos ?? Number.MAX_SAFE_INTEGER);
673
- const pb = Number(b?.pos ?? Number.MAX_SAFE_INTEGER);
674
- if (pa !== pb) return pa - pb;
675
- return String(a?.id || "").localeCompare(String(b?.id || ""));
676
- }
677
-
678
- function dateFromTrelloObjectId(value) {
679
- const idValue = String(value || "");
680
- if (!/^[0-9a-fA-F]{8}/.test(idValue)) return null;
681
- const seconds = Number.parseInt(idValue.slice(0, 8), 16);
682
- if (!Number.isFinite(seconds)) return null;
683
- return new Date(seconds * 1000).toISOString();
684
- }
685
-
686
- function cleanText(value) {
687
- return String(value ?? "").replace(/\r\n/g, "\n").trim();
688
- }
689
-
690
- function trelloScopedId(prefix, value) {
691
- return `trello-${prefix}-${String(value || id()).replace(/[^a-zA-Z0-9_-]/g, "-")}`;
692
- }