@starlove/orbit 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/.dockerignore +13 -0
  2. package/Dockerfile +34 -0
  3. package/README.md +216 -0
  4. package/SKILL-ORBIT.md +94 -0
  5. package/build.mjs +204 -0
  6. package/dist/full/apple-touch-icon.png +0 -0
  7. package/dist/full/cursors/live_arrow.cur +0 -0
  8. package/dist/full/cursors/live_link.cur +0 -0
  9. package/dist/full/cursors/live_move.cur +0 -0
  10. package/dist/full/cursors/live_select.cur +0 -0
  11. package/dist/full/cursors/live_unavail.cur +0 -0
  12. package/dist/full/favicon-32x32.png +0 -0
  13. package/dist/full/index.html +314 -0
  14. package/dist/full/js/app.70be7d06.js +554 -0
  15. package/dist/full/js/app.70be7d06.js.map +7 -0
  16. package/dist/full/orbits-favicon.png +0 -0
  17. package/dist/full/orbits-logo-bg.png +0 -0
  18. package/dist/full/orbits-logo.png +0 -0
  19. package/dist/full/styles.c2e3d20a.css +4892 -0
  20. package/dist/full/version.json +6 -0
  21. package/docs/AGENT_PROTOCOL.md +379 -0
  22. package/docs/MCP_SETUP.md +49 -0
  23. package/docs/mcp-client.example.json +13 -0
  24. package/package.json +62 -0
  25. package/pnpm-workspace.yaml +2 -0
  26. package/public/apple-touch-icon.png +0 -0
  27. package/public/cursors/live_arrow.cur +0 -0
  28. package/public/cursors/live_link.cur +0 -0
  29. package/public/cursors/live_move.cur +0 -0
  30. package/public/cursors/live_select.cur +0 -0
  31. package/public/cursors/live_unavail.cur +0 -0
  32. package/public/favicon-32x32.png +0 -0
  33. package/public/index.html +314 -0
  34. package/public/js/api.js +35 -0
  35. package/public/js/app.js +35 -0
  36. package/public/js/board-menu.js +145 -0
  37. package/public/js/config.js +19 -0
  38. package/public/js/create-card.js +153 -0
  39. package/public/js/dom.js +19 -0
  40. package/public/js/drawer.js +137 -0
  41. package/public/js/format.js +108 -0
  42. package/public/js/kanban.js +475 -0
  43. package/public/js/local-backend.js +934 -0
  44. package/public/js/main.js +142 -0
  45. package/public/js/modal.js +31 -0
  46. package/public/js/router.js +114 -0
  47. package/public/js/search.js +53 -0
  48. package/public/js/settings-tabs.js +7 -0
  49. package/public/js/settings.js +931 -0
  50. package/public/js/sse.js +156 -0
  51. package/public/js/starscape.js +315 -0
  52. package/public/js/state.js +54 -0
  53. package/public/js/ticket-detail.js +766 -0
  54. package/public/js/toast.js +74 -0
  55. package/public/js/transport.js +22 -0
  56. package/public/js/unread.js +121 -0
  57. package/public/orbits-favicon.png +0 -0
  58. package/public/orbits-logo-bg.png +0 -0
  59. package/public/orbits-logo.png +0 -0
  60. package/public/styles.css +4892 -0
  61. package/src/cli/orbit.js +438 -0
  62. package/src/core/agent.js +262 -0
  63. package/src/core/auth.js +49 -0
  64. package/src/core/backups.js +136 -0
  65. package/src/core/board.js +112 -0
  66. package/src/core/boards.js +310 -0
  67. package/src/core/bootstrap.js +162 -0
  68. package/src/core/db.js +237 -0
  69. package/src/core/delete-board.js +53 -0
  70. package/src/core/events.js +314 -0
  71. package/src/core/http.js +81 -0
  72. package/src/core/paths.js +25 -0
  73. package/src/core/provision-repo-board.js +113 -0
  74. package/src/core/queries.js +246 -0
  75. package/src/core/registry.js +207 -0
  76. package/src/core/relations.js +163 -0
  77. package/src/core/router.js +414 -0
  78. package/src/core/search.js +70 -0
  79. package/src/core/seed.js +226 -0
  80. package/src/core/snapshots.js +255 -0
  81. package/src/core/states.js +167 -0
  82. package/src/core/system-picker.js +93 -0
  83. package/src/core/tickets.js +344 -0
  84. package/src/core/util.js +105 -0
  85. package/src/mcp-server.js +773 -0
  86. package/src/server.js +48 -0
@@ -0,0 +1,142 @@
1
+ // Application entry point. Imports `starscape.js` for its side effects
2
+ // (canvas + theme toggle init), wires the topbar buttons and global
3
+ // keyboard/click listeners, and kicks off the bootstrap → router activation
4
+ // sequence. The reusable `load` / `render` helpers live in app.js so feature
5
+ // modules can import them without creating cycles back through this entry.
6
+
7
+ import "./starscape.js";
8
+
9
+ import {
10
+ $,
11
+ boardMenuBtn,
12
+ boardFlyout,
13
+ drawer,
14
+ drawerBackdrop,
15
+ createFlyoutBackdrop,
16
+ searchInput
17
+ } from "./dom.js";
18
+ import { state } from "./state.js";
19
+ import { api, withBoardQuery } from "./api.js";
20
+ import { toast, debounce } from "./toast.js";
21
+ import { navigate, applyRoute, syncUrlFromState, setRouterReady } from "./router.js";
22
+ import {
23
+ openBoardFlyout,
24
+ closeBoardFlyout,
25
+ closeMenuFlyouts
26
+ } from "./board-menu.js";
27
+ import {
28
+ openCreateFlyout,
29
+ closeCreateFlyout,
30
+ isCreateFlyoutOpen
31
+ } from "./create-card.js";
32
+ import { enableKanbanDragScroll } from "./kanban.js";
33
+ import { runSearch, hideSearch } from "./search.js";
34
+ import { load } from "./app.js";
35
+ import { edition } from "./config.js";
36
+ import { openModal, wireModal } from "./modal.js";
37
+
38
+ async function init() {
39
+ const welcomeDialog = document.getElementById("welcomeDialog");
40
+ wireModal(welcomeDialog);
41
+ if (edition === "preview") {
42
+ openModal(welcomeDialog);
43
+ }
44
+
45
+ $("#settingsBtn").addEventListener("click", async () => {
46
+ closeCreateFlyout();
47
+ closeMenuFlyouts();
48
+ navigate({
49
+ boardId: state.boardId,
50
+ view: "settings",
51
+ tab: state.settingsTab || "lanes"
52
+ });
53
+ });
54
+
55
+ boardMenuBtn?.addEventListener("click", (event) => {
56
+ event.stopPropagation();
57
+ if (!boardFlyout.hidden) {
58
+ closeBoardFlyout();
59
+ return;
60
+ }
61
+ openBoardFlyout();
62
+ });
63
+
64
+ $("#newTicketBtn").addEventListener("click", () => {
65
+ if (isCreateFlyoutOpen()) {
66
+ closeCreateFlyout();
67
+ return;
68
+ }
69
+ openCreateFlyout("");
70
+ });
71
+
72
+ createFlyoutBackdrop.addEventListener("click", () => {
73
+ closeCreateFlyout();
74
+ });
75
+
76
+ $("#drawerCloseBtn").addEventListener("click", () => {
77
+ navigate({
78
+ boardId: state.boardId,
79
+ view: "board"
80
+ });
81
+ });
82
+
83
+ $("#drawerArchiveBtn").addEventListener("click", async () => {
84
+ const ticketId = state.selectedTicketId;
85
+ if (!ticketId || state.detailMode !== "ticket") return;
86
+ const ticket = state.data?.tickets?.find((t) => t.id === ticketId);
87
+ const titleForToast = ticket?.title || "card";
88
+ await api(withBoardQuery(`/api/tickets/${encodeURIComponent(ticketId)}/archive`), { method: "POST" });
89
+ toast.error(`Archived: ${titleForToast}`);
90
+ await load();
91
+ navigate({ boardId: state.boardId, view: "board" });
92
+ });
93
+
94
+ drawerBackdrop.addEventListener("click", () => {
95
+ navigate({
96
+ boardId: state.boardId,
97
+ view: "board"
98
+ });
99
+ });
100
+
101
+ document.addEventListener("keydown", (event) => {
102
+ if (event.key !== "Escape") return;
103
+ if (!boardFlyout.hidden) {
104
+ closeMenuFlyouts();
105
+ return;
106
+ }
107
+ if (isCreateFlyoutOpen()) {
108
+ closeCreateFlyout();
109
+ return;
110
+ }
111
+ if (!drawer.classList.contains("is-open")) return;
112
+ const el = document.activeElement;
113
+ if (el?.classList?.contains("inline-title-editor") || el?.classList?.contains("inline-desc-editor")) return;
114
+ navigate({
115
+ boardId: state.boardId,
116
+ view: "board"
117
+ });
118
+ });
119
+
120
+ searchInput.addEventListener("input", debounce(runSearch, 220));
121
+ document.addEventListener("click", (event) => {
122
+ if (!event.target.closest("#topbarSearch")) hideSearch();
123
+ if (!event.target.closest(".topbar-menu")) closeMenuFlyouts();
124
+ });
125
+
126
+ enableKanbanDragScroll();
127
+
128
+ await load();
129
+
130
+ // Activate the router. If the page was loaded with a hash, restore that
131
+ // view; otherwise canonicalize the URL to reflect the current state so the
132
+ // back button has a starting point.
133
+ setRouterReady(true);
134
+ const initialHash = location.hash;
135
+ if (initialHash && initialHash !== "#" && initialHash !== "#/") {
136
+ await applyRoute();
137
+ } else {
138
+ syncUrlFromState({ replace: true });
139
+ }
140
+ }
141
+
142
+ init();
@@ -0,0 +1,31 @@
1
+ // Generic <dialog> modal utility.
2
+ //
3
+ // Usage:
4
+ // wireModal(el) — wire backdrop-click + [data-modal-close] buttons.
5
+ // openModal(el) — show the dialog as a modal (focus-trapped overlay).
6
+ // closeModal(el) — close it programmatically.
7
+ //
8
+ // Escape key is handled natively by the browser; no extra wiring needed.
9
+
10
+ export function openModal(dialogEl) {
11
+ dialogEl?.showModal();
12
+ }
13
+
14
+ export function closeModal(dialogEl) {
15
+ if (dialogEl?.open) dialogEl.close();
16
+ }
17
+
18
+ export function wireModal(dialogEl) {
19
+ if (!dialogEl) return;
20
+
21
+ // Close when the user clicks the backdrop (the dialog element itself,
22
+ // outside its inner content box).
23
+ dialogEl.addEventListener("click", (event) => {
24
+ if (event.target === dialogEl) closeModal(dialogEl);
25
+ });
26
+
27
+ // Close buttons: any element inside the dialog with [data-modal-close].
28
+ dialogEl.querySelectorAll("[data-modal-close]").forEach((btn) => {
29
+ btn.addEventListener("click", () => closeModal(dialogEl));
30
+ });
31
+ }
@@ -0,0 +1,114 @@
1
+ // Hash-based router. The URL hash is the source of truth for which view is
2
+ // shown. `navigate()` writes to it; the popstate listener replays it. The
3
+ // in-memory `state` object is still used for rendering — applyRoute() just
4
+ // mirrors the parsed hash into `state` before calling render().
5
+
6
+ import { state } from "./state.js";
7
+ import { SETTINGS_TAB_IDS, normalizeSettingsTab } from "./settings-tabs.js";
8
+ import { closeMenuFlyouts } from "./board-menu.js";
9
+ import { closeCreateFlyout } from "./create-card.js";
10
+
11
+ const Route = {
12
+ parse(hash) {
13
+ const cleaned = String(hash || "").replace(/^#/, "").replace(/^\/+/, "");
14
+ const parts = cleaned ? cleaned.split("/") : [];
15
+ const out = { boardId: "", view: "board", ticketId: "", tab: "" };
16
+ let i = 0;
17
+ if (parts[i] === "b" && parts[i + 1]) { out.boardId = decodeURIComponent(parts[i + 1]); i += 2; }
18
+ if (parts[i] === "t" && parts[i + 1]) {
19
+ out.view = "ticket";
20
+ out.ticketId = decodeURIComponent(parts[i + 1]);
21
+ } else if (parts[i] === "settings") {
22
+ out.view = "settings";
23
+ const t = parts[i + 1];
24
+ if (t && SETTINGS_TAB_IDS.has(t)) out.tab = t;
25
+ }
26
+ return out;
27
+ },
28
+ build({ boardId = "", view = "board", ticketId = "", tab = "" } = {}) {
29
+ const segs = [];
30
+ if (boardId) segs.push("b", encodeURIComponent(boardId));
31
+ if (view === "ticket" && ticketId) segs.push("t", encodeURIComponent(ticketId));
32
+ else if (view === "settings") {
33
+ segs.push("settings");
34
+ if (tab && SETTINGS_TAB_IDS.has(tab)) segs.push(tab);
35
+ }
36
+ return "#/" + segs.join("/");
37
+ },
38
+ fromState() {
39
+ let view = "board";
40
+ if (state.detailMode === "ticket" && state.selectedTicketId) view = "ticket";
41
+ else if (state.detailMode === "settings") view = "settings";
42
+ return {
43
+ boardId: state.boardId,
44
+ view,
45
+ ticketId: state.selectedTicketId,
46
+ tab: view === "settings" ? normalizeSettingsTab(state.settingsTab) : ""
47
+ };
48
+ }
49
+ };
50
+
51
+ let routerReady = false;
52
+
53
+ export function setRouterReady(value) {
54
+ routerReady = value;
55
+ }
56
+
57
+ export async function navigate(routeObj, { replace = false } = {}) {
58
+ const hash = Route.build(routeObj);
59
+ if (location.hash === hash) {
60
+ if (routerReady) await applyRoute();
61
+ return;
62
+ }
63
+ if (replace) history.replaceState(null, "", hash);
64
+ else history.pushState(null, "", hash);
65
+ if (routerReady) await applyRoute();
66
+ }
67
+
68
+ // Push or replace the URL to match the current `state` without re-rendering.
69
+ // Used after flows that already mutated state and called load()/render()
70
+ // (e.g. ticket/board creation).
71
+ export function syncUrlFromState({ replace = false } = {}) {
72
+ const hash = Route.build(Route.fromState());
73
+ if (location.hash === hash) return;
74
+ if (replace) history.replaceState(null, "", hash);
75
+ else history.pushState(null, "", hash);
76
+ }
77
+
78
+ export async function applyRoute() {
79
+ const { load, render } = await import("./app.js");
80
+ const r = Route.parse(location.hash);
81
+ const prevBoardId = state.boardId;
82
+ if (r.boardId) state.boardId = r.boardId;
83
+
84
+ if (r.view === "ticket") {
85
+ state.selectedTicketId = r.ticketId || "";
86
+ state.detailMode = r.ticketId ? "ticket" : "empty";
87
+ } else if (r.view === "settings") {
88
+ state.selectedTicketId = "";
89
+ state.detailMode = "settings";
90
+ if (r.tab) {
91
+ state.settingsTab = r.tab;
92
+ try {
93
+ localStorage.setItem("mab_settings_tab", r.tab);
94
+ } catch {
95
+ /* ignore */
96
+ }
97
+ }
98
+ } else {
99
+ state.selectedTicketId = "";
100
+ state.detailMode = "empty";
101
+ }
102
+
103
+ closeMenuFlyouts();
104
+ closeCreateFlyout();
105
+
106
+ if (r.boardId && r.boardId !== prevBoardId) {
107
+ await load();
108
+ }
109
+ await render();
110
+ }
111
+
112
+ window.addEventListener("popstate", () => {
113
+ if (routerReady) void applyRoute();
114
+ });
@@ -0,0 +1,53 @@
1
+ // Topbar search. Debounced via the input handler in main.js; queries the
2
+ // /api/search endpoint and renders a small results dropdown that navigates
3
+ // to the chosen ticket on click.
4
+
5
+ import { searchInput, searchResults } from "./dom.js";
6
+ import { state, currentBoard } from "./state.js";
7
+ import { api } from "./api.js";
8
+ import { navigate } from "./router.js";
9
+ import { escapeHtml, ticketLabel } from "./format.js";
10
+ import { closeCreateFlyout } from "./create-card.js";
11
+
12
+ export async function runSearch() {
13
+ const query = searchInput.value.trim();
14
+ if (query.length < 2) {
15
+ hideSearch();
16
+ return;
17
+ }
18
+ const board = currentBoard();
19
+ if (!board) return;
20
+ const result = await api(`/api/search?q=${encodeURIComponent(query)}&board=${encodeURIComponent(board.slug)}`);
21
+ if (result.results.length === 0) {
22
+ searchResults.hidden = false;
23
+ searchResults.innerHTML = `<div class="search-hit">No results</div>`;
24
+ return;
25
+ }
26
+ searchResults.hidden = false;
27
+ searchResults.innerHTML = result.results
28
+ .map(
29
+ (ticket) => `
30
+ <div class="search-hit" data-ticket-id="${ticket.id}">
31
+ <strong>${escapeHtml(ticketLabel(ticket))}</strong>
32
+ ${escapeHtml(ticket.title)}
33
+ </div>
34
+ `
35
+ )
36
+ .join("");
37
+ searchResults.querySelectorAll("[data-ticket-id]").forEach((hit) => {
38
+ hit.addEventListener("click", async () => {
39
+ closeCreateFlyout();
40
+ hideSearch();
41
+ navigate({
42
+ boardId: state.boardId,
43
+ view: "ticket",
44
+ ticketId: hit.dataset.ticketId
45
+ });
46
+ });
47
+ });
48
+ }
49
+
50
+ export function hideSearch() {
51
+ searchResults.hidden = true;
52
+ searchResults.innerHTML = "";
53
+ }
@@ -0,0 +1,7 @@
1
+ /** Settings drawer tab ids — must match URL segments `#/b/…/settings/:tab` and `settings.js` tab config. */
2
+ export const SETTINGS_TAB_IDS = new Set(["lanes", "ai", "notes", "journal", "repository", "archive"]);
3
+
4
+ export function normalizeSettingsTab(raw) {
5
+ if (!raw || !SETTINGS_TAB_IDS.has(raw)) return "lanes";
6
+ return raw;
7
+ }