@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,74 @@
1
+ // Toast notifications + small UI utilities (downloadJson, debounce).
2
+
3
+ import { escapeHtml } from "./format.js";
4
+
5
+ const toastIcons = {
6
+ info: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd"/></svg>`,
7
+ success: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd"/></svg>`,
8
+ warning: `<svg viewBox="0 0 24 24" fill="currentColor"><path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" clip-rule="evenodd"/></svg>`,
9
+ error: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd"/></svg>`
10
+ };
11
+
12
+ let toastContainer = null;
13
+
14
+ function ensureToastContainer() {
15
+ if (toastContainer && toastContainer.isConnected) return toastContainer;
16
+ toastContainer = document.createElement("div");
17
+ toastContainer.className = "toast-container";
18
+ document.body.append(toastContainer);
19
+ return toastContainer;
20
+ }
21
+
22
+ export function toast(message, type = "info", duration = 4000) {
23
+ const container = ensureToastContainer();
24
+
25
+ const node = document.createElement("div");
26
+ node.className = `toast toast-${type}`;
27
+ node.setAttribute("role", "status");
28
+
29
+ const icon = toastIcons[type];
30
+ node.innerHTML = `<span class="toast-icon">${icon}</span><span class="toast-msg">${escapeHtml(message)}</span><button class="toast-close" aria-label="Dismiss">&times;</button>`;
31
+
32
+ node.querySelector(".toast-close").addEventListener("click", () => dismissToast(node));
33
+
34
+ container.prepend(node);
35
+ requestAnimationFrame(() => requestAnimationFrame(() => node.classList.add("toast-enter")));
36
+
37
+ const timer = setTimeout(() => dismissToast(node), duration);
38
+ node.addEventListener("mouseenter", () => clearTimeout(timer));
39
+ node.addEventListener("mouseleave", () => {
40
+ const t2 = setTimeout(() => dismissToast(node), 1500);
41
+ node.addEventListener("mouseenter", () => clearTimeout(t2), { once: true });
42
+ });
43
+ }
44
+
45
+ toast.info = (msg, dur) => toast(msg, "info", dur);
46
+ toast.success = (msg, dur) => toast(msg, "success", dur);
47
+ toast.warning = (msg, dur) => toast(msg, "warning", dur);
48
+ toast.error = (msg, dur) => toast(msg, "error", dur);
49
+
50
+ function dismissToast(node) {
51
+ if (node.classList.contains("toast-exit")) return;
52
+ node.classList.add("toast-exit");
53
+ node.addEventListener("animationend", () => node.remove(), { once: true });
54
+ }
55
+
56
+ export function downloadJson(filename, value) {
57
+ const blob = new Blob([JSON.stringify(value, null, 2)], { type: "application/json" });
58
+ const url = URL.createObjectURL(blob);
59
+ const anchor = document.createElement("a");
60
+ anchor.href = url;
61
+ anchor.download = filename;
62
+ document.body.append(anchor);
63
+ anchor.click();
64
+ anchor.remove();
65
+ URL.revokeObjectURL(url);
66
+ }
67
+
68
+ export function debounce(fn, wait) {
69
+ let timer = 0;
70
+ return (...args) => {
71
+ clearTimeout(timer);
72
+ timer = setTimeout(() => fn(...args), wait);
73
+ };
74
+ }
@@ -0,0 +1,22 @@
1
+ // Request transport. The full edition talks to the Orbit HTTP server; a
2
+ // preview build will replace this module (or branch on `edition`) to dispatch
3
+ // the same path/body shapes to a local IndexedDB backend. Keep this file
4
+ // I/O-only — UI concerns (toasts, redirects) belong in api.js callers.
5
+
6
+ export async function request({ method = "GET", path, body, headers }) {
7
+ const finalHeaders = { "Content-Type": "application/json", ...(headers || {}) };
8
+ const response = await fetch(path, {
9
+ method,
10
+ headers: finalHeaders,
11
+ body: body !== undefined ? JSON.stringify(body) : undefined
12
+ });
13
+ const text = await response.text();
14
+ const payload = text ? JSON.parse(text) : {};
15
+ if (!response.ok) {
16
+ const error = new Error(payload.error || response.statusText);
17
+ error.payload = payload;
18
+ error.status = response.status;
19
+ throw error;
20
+ }
21
+ return payload;
22
+ }
@@ -0,0 +1,121 @@
1
+ // Per-card "unread updates" tracker. Lives client-side in localStorage so
2
+ // no backend changes are needed. Each entry tracks both:
3
+ // readAt — ISO timestamp the human last acknowledged this card
4
+ // count — number of foreign-actor events received since that ack
5
+ // Render uses a dot for count===1 and a numeric pill for count>=2.
6
+ //
7
+ // Acknowledgement happens on three paths:
8
+ // - opening the card (renderDetail in ticket-detail.js)
9
+ // - own action via SSE self-event suppression (sse.js)
10
+ // - the one-time per-board seed below, so the very first load doesn't
11
+ // flash badges on every existing card
12
+
13
+ import { state } from "./state.js";
14
+
15
+ const READ_KEY = (boardId) => `mab_card_read:${boardId}`;
16
+ const SEEDED_KEY = (boardId) => `mab_card_read_seeded:${boardId}`;
17
+
18
+ const cache = new Map();
19
+
20
+ // Normalize a stored value into { readAt, count }. Tolerates the old
21
+ // string-only shape so existing localStorage entries upgrade in place.
22
+ function normalize(raw) {
23
+ if (!raw) return { readAt: null, count: 0 };
24
+ if (typeof raw === "string") return { readAt: raw, count: 0 };
25
+ return { readAt: raw.readAt || null, count: Number(raw.count) || 0 };
26
+ }
27
+
28
+ function getMap(boardId) {
29
+ if (!boardId) return new Map();
30
+ let m = cache.get(boardId);
31
+ if (m) return m;
32
+ m = new Map();
33
+ try {
34
+ const raw = localStorage.getItem(READ_KEY(boardId));
35
+ if (raw) {
36
+ const obj = JSON.parse(raw);
37
+ for (const [k, v] of Object.entries(obj)) m.set(k, normalize(v));
38
+ }
39
+ } catch { /* ignore */ }
40
+ cache.set(boardId, m);
41
+ return m;
42
+ }
43
+
44
+ function persist(boardId) {
45
+ const m = cache.get(boardId);
46
+ if (!m) return;
47
+ try {
48
+ const obj = {};
49
+ for (const [k, v] of m) obj[k] = v;
50
+ localStorage.setItem(READ_KEY(boardId), JSON.stringify(obj));
51
+ } catch { /* ignore */ }
52
+ }
53
+
54
+ function entryFor(m, ticketId) {
55
+ let e = m.get(ticketId);
56
+ if (!e) {
57
+ e = { readAt: null, count: 0 };
58
+ m.set(ticketId, e);
59
+ }
60
+ return e;
61
+ }
62
+
63
+ export function unreadCount(ticket) {
64
+ const boardId = ticket?.board_id || state.boardId;
65
+ if (!boardId || !ticket?.id) return 0;
66
+ const m = getMap(boardId);
67
+ const e = m.get(ticket.id);
68
+ if (!e) {
69
+ // Absence: if seeded, the card appeared post-seed → flag with count 1.
70
+ // If never seeded, treat as read so first-ever load is quiet.
71
+ return localStorage.getItem(SEEDED_KEY(boardId)) === "1" ? 1 : 0;
72
+ }
73
+ return e.count;
74
+ }
75
+
76
+ export function isUnread(ticket) {
77
+ return unreadCount(ticket) > 0;
78
+ }
79
+
80
+ export function markRead(ticketId, timestamp) {
81
+ const boardId = state.boardId;
82
+ if (!boardId || !ticketId) return;
83
+ const m = getMap(boardId);
84
+ const e = entryFor(m, ticketId);
85
+ if (e.count === 0 && e.readAt && timestamp && e.readAt >= timestamp) return;
86
+ e.count = 0;
87
+ if (timestamp) e.readAt = timestamp;
88
+ persist(boardId);
89
+ }
90
+
91
+ export function bumpUnread(ticketId) {
92
+ const boardId = state.boardId;
93
+ if (!boardId || !ticketId) return;
94
+ const m = getMap(boardId);
95
+ const e = entryFor(m, ticketId);
96
+ e.count = (e.count || 0) + 1;
97
+ persist(boardId);
98
+ }
99
+
100
+ export function clearUnread(ticketId) {
101
+ const boardId = state.boardId;
102
+ if (!boardId || !ticketId) return;
103
+ const m = getMap(boardId);
104
+ if (!m.has(ticketId)) return;
105
+ m.delete(ticketId);
106
+ persist(boardId);
107
+ }
108
+
109
+ // One-time per-board seed so existing cards start "read" the first time the
110
+ // user runs this version. Runs again per-board the first time each board is
111
+ // visited.
112
+ export function seedReadIfNeeded(boardId, tickets) {
113
+ if (!boardId) return;
114
+ if (localStorage.getItem(SEEDED_KEY(boardId)) === "1") return;
115
+ const m = getMap(boardId);
116
+ for (const t of tickets || []) {
117
+ if (!m.has(t.id) && t.updated_at) m.set(t.id, { readAt: t.updated_at, count: 0 });
118
+ }
119
+ persist(boardId);
120
+ try { localStorage.setItem(SEEDED_KEY(boardId), "1"); } catch { /* ignore */ }
121
+ }
Binary file
Binary file
Binary file