@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.
- package/Dockerfile +10 -8
- package/README.md +146 -164
- package/dist/full/csp.txt +1 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-subset.woff2 +0 -0
- package/dist/full/fonts/JetBrains_Mono/README.txt +5 -0
- package/dist/full/fonts/Mona_Sans/MonaSans-subset.woff2 +0 -0
- package/{public/fonts/Open_Sans → dist/full/fonts/Mona_Sans}/OFL.txt +93 -93
- package/dist/full/fonts/Mona_Sans/README.txt +10 -0
- package/{public/fonts/JetBrains_Mono → dist/full/fonts/Newsreader}/OFL.txt +93 -93
- package/dist/full/fonts/Newsreader/README.txt +8 -0
- package/dist/full/fonts/Newsreader/newsreader.woff2 +0 -0
- package/dist/full/index.html +36 -653
- package/dist/full/js/app.BmuponPJ.js +32 -0
- package/dist/full/js/app.BmuponPJ.js.map +1 -0
- package/dist/full/js/chunk.1fJGQGbW.js +2 -0
- package/dist/full/js/chunk.1fJGQGbW.js.map +1 -0
- package/dist/full/js/chunk.5D7JO2zp.js +2 -0
- package/dist/full/js/chunk.5D7JO2zp.js.map +1 -0
- package/dist/full/js/chunk.BrNn_D79.js +2 -0
- package/dist/full/js/chunk.BrNn_D79.js.map +1 -0
- package/dist/full/js/chunk.CokJXIqM.js +2 -0
- package/dist/full/js/chunk.CokJXIqM.js.map +1 -0
- package/dist/full/js/chunk.D_Haqy6R.js +2 -0
- package/dist/full/js/chunk.D_Haqy6R.js.map +1 -0
- package/dist/full/js/chunk.RO0jsH6I.js +15 -0
- package/dist/full/js/chunk.RO0jsH6I.js.map +1 -0
- package/dist/full/js/chunk.dqWOEtBM.js +2 -0
- package/dist/full/js/chunk.dqWOEtBM.js.map +1 -0
- package/dist/full/js/warm.f260bbbc.js +15 -0
- package/dist/full/orbits-favicon.png +0 -0
- package/dist/full/orbits-logo-bg-wide.png +0 -0
- package/dist/full/orbits-logo-bg.png +0 -0
- package/dist/full/orbits-logo.png +0 -0
- package/dist/full/styles.2b154ffe.css +1 -0
- package/dist/full/vendor/starlove-ui/theme-init.js +66 -0
- package/dist/full/version.json +2 -2
- package/docs/AGENT_GUIDE.md +114 -0
- package/docs/UPDATING.md +249 -64
- package/lib/adapters/indexeddb/blobs.d.ts +3 -0
- package/lib/adapters/indexeddb/blobs.js +54 -0
- package/lib/adapters/indexeddb/database.d.ts +60 -0
- package/lib/adapters/indexeddb/database.js +126 -0
- package/lib/adapters/indexeddb/index.d.ts +5 -0
- package/lib/adapters/indexeddb/index.js +3 -0
- package/lib/adapters/indexeddb/locks.d.ts +14 -0
- package/lib/adapters/indexeddb/locks.js +40 -0
- package/lib/adapters/indexeddb/store.d.ts +40 -0
- package/lib/adapters/indexeddb/store.js +424 -0
- package/lib/adapters/indexeddb/tables.d.ts +43 -0
- package/lib/adapters/indexeddb/tables.js +127 -0
- package/lib/adapters/memory/blobs.d.ts +3 -0
- package/lib/adapters/memory/blobs.js +44 -0
- package/lib/adapters/memory/board.d.ts +41 -0
- package/lib/adapters/memory/board.js +521 -0
- package/lib/adapters/memory/index.d.ts +2 -0
- package/lib/adapters/memory/index.js +2 -0
- package/lib/adapters/memory/search.d.ts +6 -0
- package/lib/adapters/memory/search.js +30 -0
- package/lib/adapters/memory/store.d.ts +34 -0
- package/lib/adapters/memory/store.js +266 -0
- package/lib/adapters/sqlite/backups.d.ts +53 -0
- package/lib/adapters/sqlite/backups.js +232 -0
- package/lib/adapters/sqlite/blobs.d.ts +14 -0
- package/lib/adapters/sqlite/blobs.js +109 -0
- package/lib/adapters/sqlite/board.d.ts +77 -0
- package/lib/adapters/sqlite/board.js +328 -0
- package/lib/adapters/sqlite/database.d.ts +67 -0
- package/lib/adapters/sqlite/database.js +285 -0
- package/lib/adapters/sqlite/index.d.ts +3 -0
- package/lib/adapters/sqlite/index.js +2 -0
- package/lib/adapters/sqlite/lock.d.ts +11 -0
- package/lib/adapters/sqlite/lock.js +34 -0
- package/lib/adapters/sqlite/records.d.ts +54 -0
- package/lib/adapters/sqlite/records.js +546 -0
- package/lib/adapters/sqlite/schema.d.ts +54 -0
- package/lib/adapters/sqlite/schema.js +276 -0
- package/lib/adapters/sqlite/store.d.ts +39 -0
- package/lib/adapters/sqlite/store.js +385 -0
- package/lib/browser/in-page.d.ts +31 -0
- package/lib/browser/in-page.js +46 -0
- package/lib/browser/old-preview.d.ts +13 -0
- package/lib/browser/old-preview.js +80 -0
- package/lib/browser/preview-board.d.ts +8 -0
- package/lib/browser/preview-board.js +69 -0
- package/lib/cli/main.d.ts +7 -0
- package/lib/cli/main.js +343 -0
- package/lib/cli/orbit.d.ts +8 -0
- package/lib/cli/orbit.js +25 -0
- package/lib/client/http.d.ts +38 -0
- package/lib/client/http.js +334 -0
- package/lib/client/local.d.ts +26 -0
- package/lib/client/local.js +190 -0
- package/lib/client/object-url.d.ts +8 -0
- package/lib/client/object-url.js +21 -0
- package/lib/client/types.d.ts +268 -0
- package/lib/client/types.js +8 -0
- package/lib/commands/agent-tools.d.ts +31 -0
- package/lib/commands/agent-tools.js +100 -0
- package/lib/commands/index.d.ts +56 -0
- package/lib/commands/index.js +298 -0
- package/lib/commands/parse.d.ts +73 -0
- package/lib/commands/parse.js +231 -0
- package/lib/commands/present.d.ts +134 -0
- package/lib/commands/present.js +198 -0
- package/lib/commands/session.d.ts +85 -0
- package/lib/commands/session.js +138 -0
- package/lib/commands/verbs.d.ts +35 -0
- package/lib/commands/verbs.js +842 -0
- package/lib/core/actors.d.ts +18 -0
- package/lib/core/actors.js +45 -0
- package/lib/core/agent.d.ts +128 -0
- package/lib/core/agent.js +323 -0
- package/lib/core/boards.d.ts +113 -0
- package/lib/core/boards.js +288 -0
- package/lib/core/bootstrap.d.ts +47 -0
- package/lib/core/bootstrap.js +100 -0
- package/lib/core/bytes.d.ts +10 -0
- package/lib/core/bytes.js +52 -0
- package/lib/core/card-paths.d.ts +25 -0
- package/lib/core/card-paths.js +44 -0
- package/lib/core/context.d.ts +60 -0
- package/lib/core/context.js +42 -0
- package/lib/core/cross-board.d.ts +81 -0
- package/lib/core/cross-board.js +228 -0
- package/lib/core/describe.d.ts +21 -0
- package/lib/core/describe.js +81 -0
- package/lib/core/errors.d.ts +159 -0
- package/lib/core/errors.js +160 -0
- package/lib/core/files.d.ts +79 -0
- package/lib/core/files.js +262 -0
- package/lib/core/history.d.ts +20 -0
- package/lib/core/history.js +53 -0
- package/lib/core/image-types.d.ts +16 -0
- package/lib/core/image-types.js +36 -0
- package/lib/core/model.d.ts +263 -0
- package/lib/core/model.js +34 -0
- package/lib/core/nebula-link.d.ts +14 -0
- package/lib/core/nebula-link.js +39 -0
- package/lib/core/relations.d.ts +58 -0
- package/lib/core/relations.js +112 -0
- package/lib/core/search.d.ts +38 -0
- package/lib/core/search.js +92 -0
- package/lib/core/sessions.d.ts +44 -0
- package/lib/core/sessions.js +158 -0
- package/lib/core/snapshot-schema.d.ts +295 -0
- package/lib/core/snapshot-schema.js +82 -0
- package/lib/core/snapshots.d.ts +75 -0
- package/lib/core/snapshots.js +409 -0
- package/lib/core/states.d.ts +25 -0
- package/lib/core/states.js +126 -0
- package/lib/core/text.d.ts +40 -0
- package/lib/core/text.js +113 -0
- package/lib/core/tickets.d.ts +69 -0
- package/lib/core/tickets.js +311 -0
- package/lib/core/trello.d.ts +17 -0
- package/lib/core/trello.js +269 -0
- package/lib/core/verdicts.d.ts +28 -0
- package/lib/core/verdicts.js +92 -0
- package/lib/core/views.d.ts +95 -0
- package/lib/core/views.js +186 -0
- package/lib/http/app.d.ts +46 -0
- package/lib/http/app.js +491 -0
- package/lib/mcp-server.d.ts +10 -0
- package/lib/mcp-server.js +120 -0
- package/lib/node/assets.d.ts +7 -0
- package/lib/node/assets.js +68 -0
- package/lib/node/data-folder.d.ts +21 -0
- package/lib/node/data-folder.js +34 -0
- package/lib/node/env.d.ts +38 -0
- package/lib/node/env.js +49 -0
- package/lib/node/host-check.d.ts +24 -0
- package/lib/node/host-check.js +53 -0
- package/lib/node/host-services.d.ts +9 -0
- package/lib/node/host-services.js +150 -0
- package/lib/node/local.d.ts +22 -0
- package/lib/node/local.js +61 -0
- package/lib/node/migrate.d.ts +53 -0
- package/lib/node/migrate.js +635 -0
- package/lib/node/package.d.ts +7 -0
- package/lib/node/package.js +14 -0
- package/lib/node/repo-board.d.ts +49 -0
- package/lib/node/repo-board.js +245 -0
- package/lib/node/serve.d.ts +8 -0
- package/lib/node/serve.js +106 -0
- package/lib/orbit.d.ts +47 -0
- package/lib/orbit.js +44 -0
- package/lib/quiet-sqlite-warning.d.ts +1 -0
- package/lib/quiet-sqlite-warning.js +18 -0
- package/lib/store/blob-path.d.ts +6 -0
- package/lib/store/blob-path.js +12 -0
- package/lib/store/conformance.d.ts +52 -0
- package/lib/store/conformance.js +1449 -0
- package/lib/store/search-words.d.ts +10 -0
- package/lib/store/search-words.js +19 -0
- package/lib/store/types.d.ts +452 -0
- package/lib/store/types.js +34 -0
- package/llms.txt +63 -0
- package/package.json +109 -25
- package/src/cli/orbit.js +11 -658
- package/src/mcp-server.js +16 -876
- package/SKILL-ORBIT.md +0 -151
- package/build.mjs +0 -210
- package/dist/full/js/app.4e6fbe20.js +0 -952
- package/dist/full/js/app.4e6fbe20.js.map +0 -7
- package/dist/full/styles.4e60d824.css +0 -1
- package/docs/AGENT_PROTOCOL.md +0 -424
- package/docs/MCP_SETUP.md +0 -54
- package/docs/ORBIT_DISPATCH.md +0 -280
- package/docs/SS-Orbit-Dark.png +0 -0
- package/docs/SS-Orbit-Light.png +0 -0
- package/docs/mcp-client.example.json +0 -13
- package/docs/setup/orbit-init-wizard.md +0 -174
- package/public/apple-touch-icon.png +0 -0
- package/public/cursors/live_arrow.cur +0 -0
- package/public/cursors/live_link.cur +0 -0
- package/public/cursors/live_move.cur +0 -0
- package/public/cursors/live_select.cur +0 -0
- package/public/cursors/live_unavail.cur +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/OFL.txt +0 -93
- package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/OFL.txt +0 -93
- package/public/fonts/Inter/README.txt +0 -118
- package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/README.txt +0 -79
- package/public/fonts/OpenRing-Regular.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/README.txt +0 -100
- package/public/fonts/Orbitron/OFL.txt +0 -93
- package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/public/fonts/Orbitron/README.txt +0 -68
- package/public/fonts/Rajdhani/OFL.txt +0 -93
- package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/public/fonts/runescape-uf.ttf +0 -0
- package/public/index.html +0 -769
- package/public/js/actor-labels.js +0 -65
- package/public/js/api.js +0 -39
- package/public/js/app.js +0 -45
- package/public/js/artifact-drawer.js +0 -79
- package/public/js/board-export.js +0 -161
- package/public/js/board-menu.js +0 -374
- package/public/js/card-actions.js +0 -545
- package/public/js/config.js +0 -20
- package/public/js/create-card.js +0 -190
- package/public/js/dom.js +0 -19
- package/public/js/drawer.js +0 -273
- package/public/js/font-preference.js +0 -230
- package/public/js/format.js +0 -433
- package/public/js/help-menu.js +0 -31
- package/public/js/icons.js +0 -16
- package/public/js/kanban.js +0 -694
- package/public/js/local-backend.js +0 -1015
- package/public/js/main.js +0 -161
- package/public/js/modal.js +0 -33
- package/public/js/motion-preference.js +0 -64
- package/public/js/panel.js +0 -58
- package/public/js/router.js +0 -134
- package/public/js/search.js +0 -56
- package/public/js/settings-tabs.js +0 -7
- package/public/js/settings.js +0 -1634
- package/public/js/sse.js +0 -159
- package/public/js/starscape.js +0 -332
- package/public/js/state.js +0 -129
- package/public/js/text-editing.js +0 -128
- package/public/js/theme-preference.js +0 -95
- package/public/js/ticket-detail.js +0 -1516
- package/public/js/ticket-typeahead.js +0 -49
- package/public/js/toast.js +0 -89
- package/public/js/topbar-notes.js +0 -155
- package/public/js/transport.js +0 -22
- package/public/js/typeahead.js +0 -206
- package/public/js/unread.js +0 -121
- package/public/js/update-check.js +0 -58
- package/public/js/url-routes.js +0 -94
- package/public/orbits-favicon.png +0 -0
- package/public/orbits-logo-bg-wide.png +0 -0
- package/public/orbits-logo-bg.png +0 -0
- package/public/orbits-logo.png +0 -0
- package/public/styles.css +0 -7709
- package/src/core/agent.js +0 -387
- package/src/core/agents-md.js +0 -109
- package/src/core/artifacts.js +0 -149
- package/src/core/attachments.js +0 -238
- package/src/core/auth.js +0 -85
- package/src/core/backups.js +0 -142
- package/src/core/board.js +0 -148
- package/src/core/boards.js +0 -409
- package/src/core/bootstrap.js +0 -170
- package/src/core/css-bundle.js +0 -30
- package/src/core/db.js +0 -315
- package/src/core/delete-board.js +0 -163
- package/src/core/dispatch.js +0 -1007
- package/src/core/events.js +0 -314
- package/src/core/http.js +0 -115
- package/src/core/migrate.js +0 -198
- package/src/core/paths.js +0 -55
- package/src/core/provision-repo-board.js +0 -119
- package/src/core/queries.js +0 -246
- package/src/core/registry.js +0 -211
- package/src/core/relations.js +0 -163
- package/src/core/review-verdicts.js +0 -138
- package/src/core/router.js +0 -556
- package/src/core/search.js +0 -183
- package/src/core/seed.js +0 -234
- package/src/core/snapshots.js +0 -692
- package/src/core/states.js +0 -167
- package/src/core/system-picker.js +0 -109
- package/src/core/tickets.js +0 -400
- package/src/core/util.js +0 -106
- package/src/core/version-check.js +0 -154
- package/src/mcp/orbit-client.js +0 -287
- 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
|
+
}
|