@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,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boards: making one with its lanes, its settings, its journal, its context
|
|
3
|
+
* for agents, and deleting it.
|
|
4
|
+
*/
|
|
5
|
+
import { isAgent } from "./context.js";
|
|
6
|
+
import { OrbitError, isOrbitError } from "./errors.js";
|
|
7
|
+
import { record } from "./history.js";
|
|
8
|
+
import { normalizeEntryType, requiredString, slugify, asText } from "./text.js";
|
|
9
|
+
import { Lanes, ticketView } from "./views.js";
|
|
10
|
+
const LANES_WITH_AI = [
|
|
11
|
+
["Backlog", null],
|
|
12
|
+
["Todo", null],
|
|
13
|
+
["AI Ready", "ai_ready"],
|
|
14
|
+
["In Progress", "in_progress"],
|
|
15
|
+
["Review", "review"],
|
|
16
|
+
["Done", "done"],
|
|
17
|
+
["Cancelled", null]
|
|
18
|
+
];
|
|
19
|
+
function startingLanes(input, aiEnabled) {
|
|
20
|
+
if (Array.isArray(input.states)) {
|
|
21
|
+
return input.states.filter(Array.isArray).map((pair) => [String(pair[0]), (pair[1] ?? null)]);
|
|
22
|
+
}
|
|
23
|
+
return aiEnabled ? LANES_WITH_AI : LANES_WITH_AI.filter(([, role]) => role !== "ai_ready");
|
|
24
|
+
}
|
|
25
|
+
/** Makes a board with its lanes, and lists it. Answers the board's own record. */
|
|
26
|
+
export async function createBoard(c, input) {
|
|
27
|
+
const name = requiredString(input.name, "name");
|
|
28
|
+
const slug = slugify(input.slug || name);
|
|
29
|
+
if (await c.store.boards.bySlug(slug))
|
|
30
|
+
throw new OrbitError("board_slug_taken");
|
|
31
|
+
const at = c.clock.now();
|
|
32
|
+
const aiEnabled = input.ai_enabled === undefined ? true : Boolean(input.ai_enabled);
|
|
33
|
+
const board = {
|
|
34
|
+
id: c.ids.next(),
|
|
35
|
+
slug,
|
|
36
|
+
name,
|
|
37
|
+
repo_url: typeof input.repo_url === "string" ? input.repo_url : "",
|
|
38
|
+
default_branch: typeof input.default_branch === "string" && input.default_branch ? input.default_branch : "main",
|
|
39
|
+
project_notes: "",
|
|
40
|
+
agent_instructions: "",
|
|
41
|
+
ai_enabled: aiEnabled ? 1 : 0,
|
|
42
|
+
created_at: at,
|
|
43
|
+
updated_at: at
|
|
44
|
+
};
|
|
45
|
+
await c.store.boards.create(board, async (w) => {
|
|
46
|
+
let position = 0;
|
|
47
|
+
for (const [laneName, role] of startingLanes(input, aiEnabled)) {
|
|
48
|
+
await w.states.insert({ id: c.ids.next(), board_id: board.id, name: laneName, position: position++, is_default: laneName === "Todo" ? 1 : 0, role, created_at: at });
|
|
49
|
+
}
|
|
50
|
+
if (aiEnabled)
|
|
51
|
+
await ensureAgentLanes(w, c, board.id);
|
|
52
|
+
await record(w, c, "board_created", null, { board_id: board.id, slug });
|
|
53
|
+
});
|
|
54
|
+
c.changed(board.id);
|
|
55
|
+
return board;
|
|
56
|
+
}
|
|
57
|
+
const AGENT_LANES = [
|
|
58
|
+
{ name: "AI Ready", role: "ai_ready" },
|
|
59
|
+
{ name: "In Progress", role: "in_progress" },
|
|
60
|
+
{ name: "Review", role: "review" }
|
|
61
|
+
];
|
|
62
|
+
function desiredPosition(states, role) {
|
|
63
|
+
if (role === "ai_ready") {
|
|
64
|
+
const todo = states.find((state) => state.name === "Todo");
|
|
65
|
+
if (todo)
|
|
66
|
+
return todo.position + 1;
|
|
67
|
+
const inProgress = states.find((state) => state.role === "in_progress");
|
|
68
|
+
if (inProgress)
|
|
69
|
+
return inProgress.position;
|
|
70
|
+
}
|
|
71
|
+
return states.reduce((max, state) => Math.max(max, state.position), -1) + 1;
|
|
72
|
+
}
|
|
73
|
+
/** In position order, ties by when each was written (the list's own order), with positions 0 to n-1. */
|
|
74
|
+
function compact(states) {
|
|
75
|
+
const order = states.map((state, index) => ({ state, index })).sort((a, b) => a.state.position - b.state.position || a.index - b.index);
|
|
76
|
+
order.forEach(({ state }, position) => {
|
|
77
|
+
state.position = position;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Makes sure the lanes the agent flow needs exist: AI Ready right after Todo
|
|
82
|
+
* (so taking work has a predictable lane), In Progress and Review. Changes
|
|
83
|
+
* only what isn't so.
|
|
84
|
+
*/
|
|
85
|
+
export async function ensureAgentLanes(w, c, boardId) {
|
|
86
|
+
const before = await w.states.list();
|
|
87
|
+
const states = before.map((state) => ({ ...state }));
|
|
88
|
+
const added = [];
|
|
89
|
+
for (const lane of AGENT_LANES) {
|
|
90
|
+
const existing = states.find((state) => state.role === lane.role);
|
|
91
|
+
if (existing) {
|
|
92
|
+
if (lane.role !== "ai_ready" || existing.position === desiredPosition(states, lane.role))
|
|
93
|
+
continue;
|
|
94
|
+
existing.position = -1;
|
|
95
|
+
compact(states);
|
|
96
|
+
const position = desiredPosition(states, lane.role);
|
|
97
|
+
for (const state of states)
|
|
98
|
+
if (state !== existing && state.position >= position)
|
|
99
|
+
state.position += 1;
|
|
100
|
+
existing.position = position;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const position = desiredPosition(states, lane.role);
|
|
104
|
+
for (const state of states)
|
|
105
|
+
if (state.position >= position)
|
|
106
|
+
state.position += 1;
|
|
107
|
+
const state = { id: c.ids.next(), board_id: boardId, name: lane.name, position, is_default: 0, role: lane.role, created_at: c.clock.now() };
|
|
108
|
+
states.push(state);
|
|
109
|
+
added.push(state);
|
|
110
|
+
}
|
|
111
|
+
for (const state of states) {
|
|
112
|
+
const old = before.find((b) => b.id === state.id);
|
|
113
|
+
if (old && old.position !== state.position)
|
|
114
|
+
await w.states.update(state.id, { position: state.position });
|
|
115
|
+
}
|
|
116
|
+
for (const state of added)
|
|
117
|
+
await w.states.insert(state);
|
|
118
|
+
}
|
|
119
|
+
const BOARD_FIELDS = ["name", "repo_url", "default_branch", "project_notes", "agent_instructions"];
|
|
120
|
+
/** Changes a board's settings; turning AI on adds the agent lanes. */
|
|
121
|
+
export async function updateBoard(c, input) {
|
|
122
|
+
const patch = {};
|
|
123
|
+
const applied = [];
|
|
124
|
+
for (const field of BOARD_FIELDS) {
|
|
125
|
+
const value = input[field];
|
|
126
|
+
if (value === undefined)
|
|
127
|
+
continue;
|
|
128
|
+
patch[field] = field === "name" ? requiredString(value, "name") : asText(value ?? "");
|
|
129
|
+
applied.push(field);
|
|
130
|
+
}
|
|
131
|
+
const aiTurningOn = input.ai_enabled !== undefined && Boolean(input.ai_enabled);
|
|
132
|
+
if (input.ai_enabled !== undefined) {
|
|
133
|
+
patch.ai_enabled = input.ai_enabled ? 1 : 0;
|
|
134
|
+
applied.push("ai_enabled");
|
|
135
|
+
}
|
|
136
|
+
if (!applied.length)
|
|
137
|
+
return c.data.read((r) => r.board.get());
|
|
138
|
+
const after = await c.data.write(async (w) => {
|
|
139
|
+
const board = await w.board.update({ ...patch, updated_at: c.clock.now() });
|
|
140
|
+
if (aiTurningOn)
|
|
141
|
+
await ensureAgentLanes(w, c, c.board.id);
|
|
142
|
+
await record(w, c, "board_updated", null, { board_id: c.board.id, fields: applied });
|
|
143
|
+
return board;
|
|
144
|
+
});
|
|
145
|
+
c.changed(c.board.id);
|
|
146
|
+
return after;
|
|
147
|
+
}
|
|
148
|
+
/** The board as agents may read it: no notes meant for people only. */
|
|
149
|
+
function agentBoard(board) {
|
|
150
|
+
return {
|
|
151
|
+
id: board.id,
|
|
152
|
+
slug: board.slug,
|
|
153
|
+
name: board.name,
|
|
154
|
+
repo_url: board.repo_url,
|
|
155
|
+
default_branch: board.default_branch,
|
|
156
|
+
agent_instructions: board.agent_instructions,
|
|
157
|
+
ai_enabled: board.ai_enabled,
|
|
158
|
+
created_at: board.created_at,
|
|
159
|
+
updated_at: board.updated_at
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** The board's rules for agents and its journal (newest first; struck entries only when asked). */
|
|
163
|
+
export async function boardManual(c, options = {}) {
|
|
164
|
+
const [board, entries] = await c.data.read(async (r) => [await r.board.get(), await r.entries.list()]);
|
|
165
|
+
return manualOf(c, board, entries, options);
|
|
166
|
+
}
|
|
167
|
+
export function manualOf(c, board, entries, options = {}) {
|
|
168
|
+
const newestFirst = [...entries].reverse();
|
|
169
|
+
const manual = {
|
|
170
|
+
board: agentBoard(board),
|
|
171
|
+
entries: options.includeStruck ? newestFirst : newestFirst.filter((entry) => !entry.struck_at)
|
|
172
|
+
};
|
|
173
|
+
if (!isAgent(c.actor)) {
|
|
174
|
+
const { app_root, data_dir, data_dir_default, mcp_server_path } = c.deployment;
|
|
175
|
+
manual.deployment = { app_root, data_dir, data_dir_default, mcp_server_path, db_path: c.board.db_path };
|
|
176
|
+
}
|
|
177
|
+
return manual;
|
|
178
|
+
}
|
|
179
|
+
/** A decision or a lesson in the board's journal, optionally about a card. */
|
|
180
|
+
export async function createEntry(c, input) {
|
|
181
|
+
const type = normalizeEntryType(input.type);
|
|
182
|
+
const title = requiredString(input.title, "title");
|
|
183
|
+
const ticketId = typeof input.ticket_id === "string" && input.ticket_id ? input.ticket_id : null;
|
|
184
|
+
const entry = await c.data.write(async (w) => {
|
|
185
|
+
const at = c.clock.now();
|
|
186
|
+
const made = {
|
|
187
|
+
id: c.ids.next(),
|
|
188
|
+
board_id: c.board.id,
|
|
189
|
+
type,
|
|
190
|
+
title,
|
|
191
|
+
body: typeof input.body === "string" ? input.body : "",
|
|
192
|
+
ticket_id: ticketId,
|
|
193
|
+
created_by: c.actor.name,
|
|
194
|
+
created_at: at,
|
|
195
|
+
updated_at: at,
|
|
196
|
+
struck_at: null
|
|
197
|
+
};
|
|
198
|
+
await w.entries.insert(made);
|
|
199
|
+
await record(w, c, "board_entry_created", ticketId, { board_id: c.board.id, entry_id: made.id, type });
|
|
200
|
+
return made;
|
|
201
|
+
});
|
|
202
|
+
c.changed(c.board.id);
|
|
203
|
+
return entry;
|
|
204
|
+
}
|
|
205
|
+
/** Strikes a journal entry, or brings it back. */
|
|
206
|
+
export async function strikeEntry(c, entryId, input) {
|
|
207
|
+
const strike = Boolean(input.struck);
|
|
208
|
+
const entry = await c.data.write(async (w) => {
|
|
209
|
+
const before = await w.entries.get(entryId);
|
|
210
|
+
if (!before)
|
|
211
|
+
throw new OrbitError("board_entry_not_found");
|
|
212
|
+
const at = c.clock.now();
|
|
213
|
+
const after = await w.entries.update(entryId, { struck_at: strike ? (before.struck_at ?? at) : null, updated_at: at });
|
|
214
|
+
await record(w, c, strike ? "board_entry_struck" : "board_entry_restored", before.ticket_id, { board_id: c.board.id, entry_id: before.id, type: before.type });
|
|
215
|
+
return after;
|
|
216
|
+
});
|
|
217
|
+
c.changed(c.board.id);
|
|
218
|
+
return entry;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Deletes a struck journal entry for good. Only a person may: an agent strikes
|
|
222
|
+
* an entry, which takes it out of agents' context and leaves the person to
|
|
223
|
+
* decide. An entry still live is refused, so nothing goes in one step.
|
|
224
|
+
*/
|
|
225
|
+
export async function deleteEntry(c, entryId) {
|
|
226
|
+
if (isAgent(c.actor))
|
|
227
|
+
throw new OrbitError("forbidden", { message: "Deleting a journal entry is the person's call, not an agent's: strike it instead." });
|
|
228
|
+
await c.data.write(async (w) => {
|
|
229
|
+
const before = await w.entries.get(entryId);
|
|
230
|
+
if (!before)
|
|
231
|
+
throw new OrbitError("board_entry_not_found");
|
|
232
|
+
if (!before.struck_at)
|
|
233
|
+
throw new OrbitError("entry_not_struck", { message: "Strike the entry before you delete it." });
|
|
234
|
+
await w.entries.remove(entryId);
|
|
235
|
+
await record(w, c, "board_entry_deleted", before.ticket_id, { board_id: c.board.id, entry_id: before.id, type: before.type });
|
|
236
|
+
});
|
|
237
|
+
c.changed(c.board.id);
|
|
238
|
+
return { ok: true, deleted_id: entryId };
|
|
239
|
+
}
|
|
240
|
+
/** A board's archived cards, most recently archived first. */
|
|
241
|
+
export async function archivedTickets(c) {
|
|
242
|
+
return c.data.read(async (r) => {
|
|
243
|
+
const [lanes, tickets] = await Promise.all([Lanes.of(r), r.tickets.list({ archived: true })]);
|
|
244
|
+
return tickets
|
|
245
|
+
.sort((a, b) => (b.archived_at ?? "").localeCompare(a.archived_at ?? ""))
|
|
246
|
+
.map((ticket) => ticketView(ticket, lanes, c.board));
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/** Every board, by name: a small list without opening any board. */
|
|
250
|
+
export async function listBoards(c) {
|
|
251
|
+
const agent = isAgent(c.actor);
|
|
252
|
+
return {
|
|
253
|
+
boards: (await c.store.boards.list()).map((row) => ({
|
|
254
|
+
id: row.id,
|
|
255
|
+
slug: row.slug,
|
|
256
|
+
name: row.name,
|
|
257
|
+
repo_url: row.repo_url,
|
|
258
|
+
default_branch: row.default_branch,
|
|
259
|
+
...(agent ? {} : { db_path: row.db_path }),
|
|
260
|
+
last_active_at: row.last_active_at
|
|
261
|
+
}))
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Deletes a board once its slug is typed back. A copy is kept first where the
|
|
266
|
+
* storage can make one. With `delete_files: false` the board leaves the list
|
|
267
|
+
* and its data stays. Never an agent's, as with a replace: refused before
|
|
268
|
+
* anything is read, whatever the transport (the JSON API, or a host's
|
|
269
|
+
* in-process client).
|
|
270
|
+
*/
|
|
271
|
+
export async function deleteBoard(c, board, input) {
|
|
272
|
+
if (isAgent(c.actor))
|
|
273
|
+
throw new OrbitError("forbidden", { message: "Deleting a board is the person's call, not an agent's: ask them to delete it in Orbit." });
|
|
274
|
+
if (asText(input.confirm_slug ?? "").trim() !== board.slug)
|
|
275
|
+
throw new OrbitError("board_slug_confirmation_required");
|
|
276
|
+
const deleteFiles = input.delete_files !== false;
|
|
277
|
+
// A board whose file is gone has nothing to copy, and deleting it is the way out.
|
|
278
|
+
if (deleteFiles)
|
|
279
|
+
await c.store.backups?.board(board.id, "pre-board-delete").catch((error) => {
|
|
280
|
+
if (!(isOrbitError(error) && error.code === "board_file_missing"))
|
|
281
|
+
throw error;
|
|
282
|
+
});
|
|
283
|
+
await c.store.backups?.directory("pre-board-delete");
|
|
284
|
+
const { removed } = await c.store.boards.remove(board.id, { keepData: !deleteFiles });
|
|
285
|
+
if (deleteFiles)
|
|
286
|
+
await c.blobs.removeBoard(board.id);
|
|
287
|
+
return { ok: true, deleted_board_id: board.id, deleted_board_slug: board.slug, deleted_files: deleteFiles, artifacts: removed.map((path) => ({ path, removed: true })) };
|
|
288
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the app loads first: the list of boards, and one board whole (its
|
|
3
|
+
* lanes, open cards with their labels and counts, labels and journal). The
|
|
4
|
+
* board is the one asked for, else the one used last.
|
|
5
|
+
*/
|
|
6
|
+
import type { CrossCard } from "./cross-board.ts";
|
|
7
|
+
import type { Ctx } from "./context.ts";
|
|
8
|
+
import type { Actor, Board, BoardEntry, Id, Label, State } from "./model.ts";
|
|
9
|
+
import type { CompactTicket, TicketView } from "./views.ts";
|
|
10
|
+
export type PublicActor = Pick<Actor, "id" | "name" | "type" | "role" | "permissions">;
|
|
11
|
+
export declare function publicActor(actor: Actor): PublicActor;
|
|
12
|
+
export interface BootstrapTicket extends TicketView {
|
|
13
|
+
labels: {
|
|
14
|
+
ticket_id: Id;
|
|
15
|
+
id: Id;
|
|
16
|
+
name: string;
|
|
17
|
+
color: string;
|
|
18
|
+
}[];
|
|
19
|
+
comment_count: number;
|
|
20
|
+
child_count: number;
|
|
21
|
+
parent_ticket: CompactTicket | null;
|
|
22
|
+
/** Cards on other boards this card waits on; present only when there are some. */
|
|
23
|
+
waiting_on?: CrossCard[];
|
|
24
|
+
}
|
|
25
|
+
export interface Bootstrap {
|
|
26
|
+
actor: PublicActor;
|
|
27
|
+
active_board_id?: Id;
|
|
28
|
+
/**
|
|
29
|
+
* Where the board's data is, for the person's own app; empty for an agent.
|
|
30
|
+
* `older_data` is there, true, only while no board exists and an older
|
|
31
|
+
* Orbit's boards wait for `orbit migrate` (the welcome says so).
|
|
32
|
+
*/
|
|
33
|
+
meta: {
|
|
34
|
+
db_path?: string | null;
|
|
35
|
+
export_dir?: string;
|
|
36
|
+
older_data?: true;
|
|
37
|
+
};
|
|
38
|
+
boards: Board[];
|
|
39
|
+
board_entries: BoardEntry[];
|
|
40
|
+
states: State[];
|
|
41
|
+
labels: Label[];
|
|
42
|
+
tickets: BootstrapTicket[];
|
|
43
|
+
}
|
|
44
|
+
export declare function bootstrap(c: Ctx, request?: {
|
|
45
|
+
board_id?: string | null;
|
|
46
|
+
board?: string | null;
|
|
47
|
+
}): Promise<Bootstrap>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the app loads first: the list of boards, and one board whole (its
|
|
3
|
+
* lanes, open cards with their labels and counts, labels and journal). The
|
|
4
|
+
* board is the one asked for, else the one used last.
|
|
5
|
+
*/
|
|
6
|
+
import { isAgent } from "./context.js";
|
|
7
|
+
import { crossBlockers } from "./cross-board.js";
|
|
8
|
+
import { Lanes, byCodePoint, compactTicket, ticketView } from "./views.js";
|
|
9
|
+
export function publicActor(actor) {
|
|
10
|
+
return { id: actor.id, name: actor.name, type: actor.type, role: actor.role, permissions: actor.permissions };
|
|
11
|
+
}
|
|
12
|
+
/** A listed board as the boards list shows one that isn't loaded: its settings left blank. */
|
|
13
|
+
function summary(row) {
|
|
14
|
+
return {
|
|
15
|
+
id: row.id,
|
|
16
|
+
slug: row.slug,
|
|
17
|
+
name: row.name,
|
|
18
|
+
repo_url: row.repo_url,
|
|
19
|
+
default_branch: row.default_branch,
|
|
20
|
+
project_notes: "",
|
|
21
|
+
agent_instructions: "",
|
|
22
|
+
ai_enabled: 0,
|
|
23
|
+
created_at: row.created_at,
|
|
24
|
+
updated_at: row.updated_at
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export async function bootstrap(c, request = {}) {
|
|
28
|
+
const agent = isAgent(c.actor);
|
|
29
|
+
const rows = await c.store.boards.list();
|
|
30
|
+
const meta = (dbPath) => (agent ? {} : { db_path: dbPath, export_dir: c.deployment.export_dir });
|
|
31
|
+
if (!rows.length) {
|
|
32
|
+
const older = !agent && c.deployment.older_data ? { older_data: true } : {};
|
|
33
|
+
return { actor: publicActor(c.actor), meta: { ...meta(null), ...older }, boards: [], board_entries: [], states: [], labels: [], tickets: [] };
|
|
34
|
+
}
|
|
35
|
+
const chosen = (request.board_id ? rows.find((row) => row.id === request.board_id) : undefined) ??
|
|
36
|
+
(request.board ? rows.find((row) => row.slug === request.board) : undefined) ??
|
|
37
|
+
(await c.store.boards.latest()) ??
|
|
38
|
+
rows[0];
|
|
39
|
+
if (!chosen)
|
|
40
|
+
throw new Error("A board list with boards has a first board.");
|
|
41
|
+
await c.store.boards.touch(chosen.id, c.clock.now());
|
|
42
|
+
const data = await c.store.board(chosen.id);
|
|
43
|
+
const loaded = await data.read(async (r) => {
|
|
44
|
+
const [board, states, tickets, labels, links, comments, entries, crossLinks] = await Promise.all([
|
|
45
|
+
r.board.get(),
|
|
46
|
+
r.states.list(),
|
|
47
|
+
r.tickets.list({ archived: false }),
|
|
48
|
+
r.labels.list(),
|
|
49
|
+
r.ticketLabels.list(),
|
|
50
|
+
r.comments.countByTicket(),
|
|
51
|
+
r.entries.list(),
|
|
52
|
+
r.crossBoardRelations.list()
|
|
53
|
+
]);
|
|
54
|
+
const open = new Map(tickets.map((ticket) => [ticket.id, ticket]));
|
|
55
|
+
const missing = [...new Set(tickets.map((ticket) => ticket.parent_ticket_id).filter((id) => id !== null && !open.has(id)))];
|
|
56
|
+
const archivedParents = missing.length ? await r.tickets.list({ ids: missing }) : [];
|
|
57
|
+
return { board, states, tickets, labels, links, comments, entries, crossLinks, parents: new Map([...open, ...archivedParents.map((t) => [t.id, t])]) };
|
|
58
|
+
});
|
|
59
|
+
// Cards waiting on other boards' cards: one read per other board, after this board's own.
|
|
60
|
+
const waiting = await crossBlockers(c, loaded.crossLinks);
|
|
61
|
+
const lanes = new Lanes(loaded.states);
|
|
62
|
+
const labelById = new Map(loaded.labels.map((label) => [label.id, label]));
|
|
63
|
+
const labelsOf = new Map();
|
|
64
|
+
for (const link of loaded.links) {
|
|
65
|
+
const label = labelById.get(link.label_id);
|
|
66
|
+
if (!label)
|
|
67
|
+
continue;
|
|
68
|
+
const list = labelsOf.get(link.ticket_id) ?? [];
|
|
69
|
+
list.push({ ticket_id: link.ticket_id, id: label.id, name: label.name, color: label.color });
|
|
70
|
+
labelsOf.set(link.ticket_id, list);
|
|
71
|
+
}
|
|
72
|
+
const commentCount = new Map(loaded.comments.map((row) => [row.ticket_id, row.count]));
|
|
73
|
+
const childCount = new Map();
|
|
74
|
+
for (const ticket of loaded.tickets)
|
|
75
|
+
if (ticket.parent_ticket_id)
|
|
76
|
+
childCount.set(ticket.parent_ticket_id, (childCount.get(ticket.parent_ticket_id) ?? 0) + 1);
|
|
77
|
+
const tickets = [...loaded.tickets]
|
|
78
|
+
.sort((a, b) => byCodePoint(b.updated_at, a.updated_at) || byCodePoint(a.id, b.id))
|
|
79
|
+
.map((ticket) => {
|
|
80
|
+
const parent = ticket.parent_ticket_id ? loaded.parents.get(ticket.parent_ticket_id) : undefined;
|
|
81
|
+
return {
|
|
82
|
+
...ticketView(ticket, lanes, loaded.board),
|
|
83
|
+
labels: labelsOf.get(ticket.id) ?? [],
|
|
84
|
+
comment_count: commentCount.get(ticket.id) ?? 0,
|
|
85
|
+
child_count: childCount.get(ticket.id) ?? 0,
|
|
86
|
+
parent_ticket: parent ? compactTicket(ticketView(parent, lanes, loaded.board)) : null,
|
|
87
|
+
...(waiting.has(ticket.id) ? { waiting_on: waiting.get(ticket.id) } : {})
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
actor: publicActor(c.actor),
|
|
92
|
+
active_board_id: chosen.id,
|
|
93
|
+
meta: meta(chosen.db_path),
|
|
94
|
+
boards: rows.map((row) => (row.id === loaded.board.id ? loaded.board : summary(row))),
|
|
95
|
+
board_entries: [...loaded.entries].reverse(),
|
|
96
|
+
states: loaded.states,
|
|
97
|
+
labels: loaded.labels,
|
|
98
|
+
tickets
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bytes without Node: base64 and SHA-256 over the Web platform's own
|
|
3
|
+
* `crypto`, so the core runs the same on a server and in a browser tab.
|
|
4
|
+
*/
|
|
5
|
+
export declare function toBase64(bytes: Uint8Array): string;
|
|
6
|
+
/** Decodes base64, skipping anything outside its alphabet (line breaks, a data: prefix's tail). */
|
|
7
|
+
export declare function fromBase64(text: string): Uint8Array;
|
|
8
|
+
export declare function sha256Hex(bytes: Uint8Array): Promise<string>;
|
|
9
|
+
export declare function utf8(text: string): Uint8Array;
|
|
10
|
+
export declare function fromUtf8(bytes: Uint8Array): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bytes without Node: base64 and SHA-256 over the Web platform's own
|
|
3
|
+
* `crypto`, so the core runs the same on a server and in a browser tab.
|
|
4
|
+
*/
|
|
5
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
6
|
+
const LOOKUP = new Map(Array.from(ALPHABET, (ch, i) => [ch, i]));
|
|
7
|
+
export function toBase64(bytes) {
|
|
8
|
+
let out = "";
|
|
9
|
+
let i = 0;
|
|
10
|
+
for (; i + 2 < bytes.length; i += 3) {
|
|
11
|
+
const n = ((bytes[i] ?? 0) << 16) | ((bytes[i + 1] ?? 0) << 8) | (bytes[i + 2] ?? 0);
|
|
12
|
+
out += ALPHABET.charAt(n >> 18) + ALPHABET.charAt((n >> 12) & 63) + ALPHABET.charAt((n >> 6) & 63) + ALPHABET.charAt(n & 63);
|
|
13
|
+
}
|
|
14
|
+
const rest = bytes.length - i;
|
|
15
|
+
if (rest === 1) {
|
|
16
|
+
const n = (bytes[i] ?? 0) << 16;
|
|
17
|
+
out += `${ALPHABET.charAt(n >> 18)}${ALPHABET.charAt((n >> 12) & 63)}==`;
|
|
18
|
+
}
|
|
19
|
+
else if (rest === 2) {
|
|
20
|
+
const n = ((bytes[i] ?? 0) << 16) | ((bytes[i + 1] ?? 0) << 8);
|
|
21
|
+
out += `${ALPHABET.charAt(n >> 18)}${ALPHABET.charAt((n >> 12) & 63)}${ALPHABET.charAt((n >> 6) & 63)}=`;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
/** Decodes base64, skipping anything outside its alphabet (line breaks, a data: prefix's tail). */
|
|
26
|
+
export function fromBase64(text) {
|
|
27
|
+
const clean = text.replace(/[^A-Za-z0-9+/]/g, "");
|
|
28
|
+
const out = new Uint8Array(Math.floor((clean.length * 3) / 4));
|
|
29
|
+
let length = 0;
|
|
30
|
+
let buffer = 0;
|
|
31
|
+
let bits = 0;
|
|
32
|
+
for (const ch of clean) {
|
|
33
|
+
buffer = (buffer << 6) | (LOOKUP.get(ch) ?? 0);
|
|
34
|
+
bits += 6;
|
|
35
|
+
if (bits >= 8) {
|
|
36
|
+
bits -= 8;
|
|
37
|
+
out[length++] = (buffer >> bits) & 0xff;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out.subarray(0, length);
|
|
41
|
+
}
|
|
42
|
+
export async function sha256Hex(bytes) {
|
|
43
|
+
// A copy on its own ArrayBuffer, which the DOM's typings of digest() require.
|
|
44
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(bytes)));
|
|
45
|
+
return [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
46
|
+
}
|
|
47
|
+
export function utf8(text) {
|
|
48
|
+
return new TextEncoder().encode(text);
|
|
49
|
+
}
|
|
50
|
+
export function fromUtf8(bytes) {
|
|
51
|
+
return new TextDecoder().decode(bytes);
|
|
52
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A card's two links to markdown, its plan (`plan_artifact_path`) and its
|
|
3
|
+
* work summary (`implementation_artifact_path`), as they are stored. A link is
|
|
4
|
+
* relative to the board's artifacts (`tickets/<card id>/plan.md`), which is
|
|
5
|
+
* how Orbit reads it back on any machine, and how no answer an agent reads
|
|
6
|
+
* ever carries the folders of the machine Orbit runs on.
|
|
7
|
+
*/
|
|
8
|
+
import type { Ctx } from "./context.ts";
|
|
9
|
+
import type { Id } from "./model.ts";
|
|
10
|
+
/** Where a board's markdown lives in its blob store. */
|
|
11
|
+
export declare const ARTIFACTS = "artifacts/";
|
|
12
|
+
/**
|
|
13
|
+
* A link as a person or an agent gives it. A full path into the board's
|
|
14
|
+
* artifacts becomes relative to them; a full path anywhere else in Orbit's
|
|
15
|
+
* data folder names none of the card's files, so it is refused. Any other
|
|
16
|
+
* text is kept as given.
|
|
17
|
+
*/
|
|
18
|
+
export declare function cardLinkFromInput(c: Ctx, boardId: Id, value: unknown): string;
|
|
19
|
+
/**
|
|
20
|
+
* A link from an export or an older Orbit's data, which was made on another
|
|
21
|
+
* machine or in another folder: a full path into a board's artifacts keeps
|
|
22
|
+
* its part inside them; any other full path names a file this Orbit can't
|
|
23
|
+
* reach, so it is left out (null, for the caller to count).
|
|
24
|
+
*/
|
|
25
|
+
export declare function cardLinkFromElsewhere(value: unknown): string | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A card's two links to markdown, its plan (`plan_artifact_path`) and its
|
|
3
|
+
* work summary (`implementation_artifact_path`), as they are stored. A link is
|
|
4
|
+
* relative to the board's artifacts (`tickets/<card id>/plan.md`), which is
|
|
5
|
+
* how Orbit reads it back on any machine, and how no answer an agent reads
|
|
6
|
+
* ever carries the folders of the machine Orbit runs on.
|
|
7
|
+
*/
|
|
8
|
+
import { OrbitError } from "./errors.js";
|
|
9
|
+
import { textOr } from "./text.js";
|
|
10
|
+
/** Where a board's markdown lives in its blob store. */
|
|
11
|
+
export const ARTIFACTS = "artifacts/";
|
|
12
|
+
const FULL_PATH = /^([a-z]:)?[\\/]/i;
|
|
13
|
+
/**
|
|
14
|
+
* A link as a person or an agent gives it. A full path into the board's
|
|
15
|
+
* artifacts becomes relative to them; a full path anywhere else in Orbit's
|
|
16
|
+
* data folder names none of the card's files, so it is refused. Any other
|
|
17
|
+
* text is kept as given.
|
|
18
|
+
*/
|
|
19
|
+
export function cardLinkFromInput(c, boardId, value) {
|
|
20
|
+
const text = textOr(value);
|
|
21
|
+
if (!FULL_PATH.test(text))
|
|
22
|
+
return text;
|
|
23
|
+
const path = text.replace(/\\/g, "/");
|
|
24
|
+
const root = c.blobs.location?.(boardId, ARTIFACTS.slice(0, -1));
|
|
25
|
+
if (root && path.startsWith(`${root}/`))
|
|
26
|
+
return path.slice(root.length + 1);
|
|
27
|
+
const data = c.deployment.data_dir;
|
|
28
|
+
if (data && (path === data || path.startsWith(`${data}/`)))
|
|
29
|
+
throw new OrbitError("artifact_path_outside_allowed_roots");
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A link from an export or an older Orbit's data, which was made on another
|
|
34
|
+
* machine or in another folder: a full path into a board's artifacts keeps
|
|
35
|
+
* its part inside them; any other full path names a file this Orbit can't
|
|
36
|
+
* reach, so it is left out (null, for the caller to count).
|
|
37
|
+
*/
|
|
38
|
+
export function cardLinkFromElsewhere(value) {
|
|
39
|
+
const text = textOr(value);
|
|
40
|
+
if (!FULL_PATH.test(text))
|
|
41
|
+
return text;
|
|
42
|
+
const inside = /\/artifacts\/(.+)$/.exec(text.replace(/\\/g, "/"));
|
|
43
|
+
return inside?.[1] && !inside[1].split("/").some((segment) => segment === ".." || segment === "." || segment === "") ? inside[1] : null;
|
|
44
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every use case runs with: the ports (storage, bytes, a clock and ids),
|
|
3
|
+
* who is acting, and, for a use case on one board, that board. The core never
|
|
4
|
+
* reaches for anything else, so the same code runs on a server, in a CLI and
|
|
5
|
+
* in a browser tab.
|
|
6
|
+
*/
|
|
7
|
+
import type { Actor, Id, RegisteredBoard, Timestamp } from "./model.ts";
|
|
8
|
+
import type { BlobStore, BoardStore, OrbitStore } from "../store/types.ts";
|
|
9
|
+
export interface Clock {
|
|
10
|
+
now(): Timestamp;
|
|
11
|
+
}
|
|
12
|
+
export interface Ids {
|
|
13
|
+
next(): Id;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Where this Orbit runs, for the person's own app (the Settings screen builds
|
|
17
|
+
* its agent-connection command from it). Never shown to an agent. A host that
|
|
18
|
+
* runs no files (the browser edition) leaves the paths empty.
|
|
19
|
+
*/
|
|
20
|
+
export interface Deployment {
|
|
21
|
+
app_root: string;
|
|
22
|
+
data_dir: string;
|
|
23
|
+
/** True when Orbit uses its default data folder, which an agent's `orbit mcp` finds without being told. */
|
|
24
|
+
data_dir_default: boolean;
|
|
25
|
+
mcp_server_path: string;
|
|
26
|
+
export_dir: string;
|
|
27
|
+
/**
|
|
28
|
+
* True when an older Orbit's boards wait in its default folder for `orbit
|
|
29
|
+
* migrate`, so an empty Orbit can say how to bring them. Its path is never
|
|
30
|
+
* shown to an agent.
|
|
31
|
+
*/
|
|
32
|
+
older_data: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface Runtime {
|
|
35
|
+
readonly store: OrbitStore;
|
|
36
|
+
readonly blobs: BlobStore;
|
|
37
|
+
readonly clock: Clock;
|
|
38
|
+
readonly ids: Ids;
|
|
39
|
+
readonly deployment: Deployment;
|
|
40
|
+
/** Called after each change that went through, with its board (automatic backups). */
|
|
41
|
+
readonly changed: (boardId: Id) => void;
|
|
42
|
+
}
|
|
43
|
+
export interface Ctx extends Runtime {
|
|
44
|
+
readonly actor: Actor;
|
|
45
|
+
}
|
|
46
|
+
/** A use case on one board: the board as the directory lists it, and its data. */
|
|
47
|
+
export interface BoardCtx extends Ctx {
|
|
48
|
+
readonly board: RegisteredBoard;
|
|
49
|
+
readonly data: BoardStore;
|
|
50
|
+
}
|
|
51
|
+
/** True when an agent is asking: its answers carry no server paths. */
|
|
52
|
+
export declare function isAgent(actor: Actor): boolean;
|
|
53
|
+
/** The context for one board, as the directory lists it. */
|
|
54
|
+
export declare function onBoard(c: Ctx, board: RegisteredBoard): Promise<BoardCtx>;
|
|
55
|
+
/** A board by its id or slug, or null. */
|
|
56
|
+
export declare function findBoard(c: Ctx, ident: string): Promise<RegisteredBoard | null>;
|
|
57
|
+
/** A board named by id first, then slug; null when neither is given; `board_not_found` when one is given and unknown. */
|
|
58
|
+
export declare function boardFromHints(c: Ctx, id: string | null | undefined, slug: string | null | undefined): Promise<RegisteredBoard | null>;
|
|
59
|
+
/** The board holding a record, looked for on `hint` first; `notFound` when no board has it. */
|
|
60
|
+
export declare function boardHolding(c: Ctx, kind: "state" | "ticket" | "entry" | "relation", id: string, hint: RegisteredBoard | null, notFound: "state_not_found" | "ticket_not_found" | "board_entry_not_found" | "relation_not_found"): Promise<BoardCtx>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every use case runs with: the ports (storage, bytes, a clock and ids),
|
|
3
|
+
* who is acting, and, for a use case on one board, that board. The core never
|
|
4
|
+
* reaches for anything else, so the same code runs on a server, in a CLI and
|
|
5
|
+
* in a browser tab.
|
|
6
|
+
*/
|
|
7
|
+
import { OrbitError } from "./errors.js";
|
|
8
|
+
/** True when an agent is asking: its answers carry no server paths. */
|
|
9
|
+
export function isAgent(actor) {
|
|
10
|
+
return actor.type === "agent";
|
|
11
|
+
}
|
|
12
|
+
/** The context for one board, as the directory lists it. */
|
|
13
|
+
export async function onBoard(c, board) {
|
|
14
|
+
return { ...c, board, data: await c.store.board(board.id) };
|
|
15
|
+
}
|
|
16
|
+
/** A board by its id or slug, or null. */
|
|
17
|
+
export async function findBoard(c, ident) {
|
|
18
|
+
return (await c.store.boards.get(ident)) ?? (await c.store.boards.bySlug(ident));
|
|
19
|
+
}
|
|
20
|
+
/** A board named by id first, then slug; null when neither is given; `board_not_found` when one is given and unknown. */
|
|
21
|
+
export async function boardFromHints(c, id, slug) {
|
|
22
|
+
if (id) {
|
|
23
|
+
const row = await c.store.boards.get(id);
|
|
24
|
+
if (!row)
|
|
25
|
+
throw new OrbitError("board_not_found");
|
|
26
|
+
return row;
|
|
27
|
+
}
|
|
28
|
+
if (slug) {
|
|
29
|
+
const row = await c.store.boards.bySlug(slug);
|
|
30
|
+
if (!row)
|
|
31
|
+
throw new OrbitError("board_not_found");
|
|
32
|
+
return row;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/** The board holding a record, looked for on `hint` first; `notFound` when no board has it. */
|
|
37
|
+
export async function boardHolding(c, kind, id, hint, notFound) {
|
|
38
|
+
const row = await c.store.locate(kind, id, hint?.id);
|
|
39
|
+
if (!row)
|
|
40
|
+
throw new OrbitError(notFound);
|
|
41
|
+
return onBoard(c, row);
|
|
42
|
+
}
|