@starlove/orbit 1.0.7 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +22 -7
  2. package/SKILL-ORBIT.md +26 -1
  3. package/dist/full/index.html +2 -2
  4. package/dist/full/js/app.a4f045fc.js +676 -0
  5. package/dist/full/js/app.a4f045fc.js.map +7 -0
  6. package/dist/full/{styles.14e9dbbd.css → styles.836872e7.css} +836 -39
  7. package/dist/full/version.json +2 -2
  8. package/docs/AGENT_PROTOCOL.md +23 -5
  9. package/docs/MCP_SETUP.md +5 -3
  10. package/docs/ORBIT_DISPATCH.md +185 -0
  11. package/docs/setup/orbit-init-wizard.md +174 -0
  12. package/package.json +2 -2
  13. package/public/js/actor-labels.js +65 -0
  14. package/public/js/app.js +8 -1
  15. package/public/js/card-actions.js +526 -0
  16. package/public/js/config.js +2 -1
  17. package/public/js/format.js +204 -10
  18. package/public/js/kanban.js +15 -0
  19. package/public/js/local-backend.js +24 -8
  20. package/public/js/router.js +3 -2
  21. package/public/js/search.js +6 -3
  22. package/public/js/settings.js +96 -26
  23. package/public/js/state.js +50 -0
  24. package/public/js/ticket-detail.js +428 -125
  25. package/public/styles.css +836 -39
  26. package/src/cli/orbit.js +217 -32
  27. package/src/core/agent.js +1 -18
  28. package/src/core/agents-md.js +15 -10
  29. package/src/core/attachments.js +238 -0
  30. package/src/core/auth.js +36 -0
  31. package/src/core/backups.js +9 -3
  32. package/src/core/board.js +31 -0
  33. package/src/core/boards.js +1 -1
  34. package/src/core/db.js +42 -2
  35. package/src/core/delete-board.js +87 -21
  36. package/src/core/dispatch.js +532 -0
  37. package/src/core/http.js +31 -11
  38. package/src/core/migrate.js +198 -0
  39. package/src/core/paths.js +6 -0
  40. package/src/core/registry.js +12 -9
  41. package/src/core/router.js +61 -5
  42. package/src/core/search.js +3 -3
  43. package/src/core/snapshots.js +14 -2
  44. package/src/mcp/orbit-client.js +81 -38
  45. package/src/mcp-server.js +190 -6
  46. package/src/server.js +20 -0
  47. package/dist/full/js/app.f4fd5e94.js +0 -604
  48. package/dist/full/js/app.f4fd5e94.js.map +0 -7
package/src/mcp-server.js CHANGED
@@ -5,6 +5,46 @@
5
5
  // "active board" state.
6
6
 
7
7
  import { createOrbitClient } from "./mcp/orbit-client.js";
8
+ import { existsSync, mkdirSync } from "node:fs";
9
+ import { dirname, join, resolve } from "node:path";
10
+ import {
11
+ archivedTicketsForBoard,
12
+ archiveTicket,
13
+ checkpointTicket,
14
+ claimNext,
15
+ completeTicket,
16
+ createBoardEntry,
17
+ createComment,
18
+ createTicket,
19
+ deleteTicket,
20
+ exportBoard,
21
+ getBoardContext,
22
+ getContextPack,
23
+ readComments,
24
+ readTicket,
25
+ getTicketBlockers,
26
+ getTicketRelations,
27
+ localAgentActor,
28
+ restoreTicket,
29
+ searchTickets,
30
+ updateBoard,
31
+ updateTicket
32
+ } from "./core/board.js";
33
+ import { closeAllConnections, createBoardSchema, openConnection } from "./core/db.js";
34
+ import {
35
+ getBoardByRegistryId,
36
+ getBoardBySlug,
37
+ getBoardByRepoPath,
38
+ insertBoard,
39
+ listBoards,
40
+ openBoardDb,
41
+ syncRegistryFromBoardDb,
42
+ touchBoardActive
43
+ } from "./core/registry.js";
44
+ import { scheduleAutomaticBoardBackup } from "./core/backups.js";
45
+ import { provisionRepoBoard } from "./core/provision-repo-board.js";
46
+ import { seedIfEmpty } from "./core/seed.js";
47
+ import { now, normalizePath } from "./core/util.js";
8
48
 
9
49
  const SERVER_INFO = {
10
50
  name: "minimal-agent-board",
@@ -12,6 +52,133 @@ const SERVER_INFO = {
12
52
  };
13
53
 
14
54
  const orbitClient = await createOrbitClient();
55
+ // Per-process session board. Set on init by walking up from cwd, mutable via
56
+ // `board_set_active`. NEVER read by anything outside this file — every
57
+ // downstream call receives an explicit ctx.
58
+ let sessionBoardRow = null;
59
+
60
+ function setSessionBoard(row) {
61
+ sessionBoardRow = row;
62
+ if (row) touchBoardActive(row.id);
63
+ }
64
+
65
+ function getSessionBoard() {
66
+ return sessionBoardRow;
67
+ }
68
+
69
+ /**
70
+ * Walk up from `startDir` toward the filesystem root, returning the first
71
+ * directory that satisfies `predicate`. Returns null if none found.
72
+ */
73
+ function walkUp(startDir, predicate) {
74
+ let dir = startDir;
75
+ while (true) {
76
+ if (predicate(dir)) return dir;
77
+ const parent = dirname(dir);
78
+ if (parent === dir) return null;
79
+ dir = parent;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Initialise the MCP session board. Resolution order:
85
+ * 1. Walk up from PROJECT_ROOT (cwd unless `orbit mcp --cwd` or env overrides it)
86
+ * for an existing `.orbit/board.db` — finds the board even when the agent
87
+ * is launched from a subdirectory or MCP config uses an absolute command.
88
+ * 2. Walk up from PROJECT_ROOT for a `.git/` directory — auto-creates a board at
89
+ * the git root on first launch.
90
+ * 3. Fall back to PROJECT_ROOT itself — supports non-git projects and global installs.
91
+ */
92
+ function initMcpSessionBoard() {
93
+ const start = normalizePath(process.env.PROJECT_ROOT ? resolve(process.env.PROJECT_ROOT) : process.cwd());
94
+
95
+ // 1. Prefer the registry row for PROJECT_ROOT (or one of its ancestors). New
96
+ // Orbit boards live in central DATA_DIR storage, so there may be no in-repo
97
+ // .orbit/board.db to discover.
98
+ const registeredRoot = walkUp(start, (dir) => Boolean(getBoardByRepoPath(normalizePath(dir))));
99
+ if (registeredRoot) {
100
+ setSessionBoard(getBoardByRepoPath(normalizePath(registeredRoot)));
101
+ return;
102
+ }
103
+
104
+ // 2. Walk up for an existing legacy board db.
105
+ const boardRoot = walkUp(start, (dir) => existsSync(join(dir, ".orbit", "board.db")));
106
+ if (boardRoot) {
107
+ const root = normalizePath(boardRoot);
108
+ const existing = getBoardByRepoPath(root);
109
+ if (existing) { setSessionBoard(existing); return; }
110
+ // db exists on disk but isn't in the registry yet — register it.
111
+ const dbPath = join(boardRoot, ".orbit", "board.db");
112
+ const db = openConnection(dbPath);
113
+ createBoardSchema(db);
114
+ const seeded = seedIfEmpty(db, root);
115
+ if (seeded) {
116
+ const t = now();
117
+ insertBoard({ id: seeded.id, slug: seeded.slug, name: seeded.name, repo_path: root, db_path: dbPath, repo_url: seeded.repo_url || "", default_branch: seeded.default_branch || "main", last_active_at: t, created_at: t, updated_at: t });
118
+ }
119
+ const fresh = getBoardByRepoPath(root);
120
+ if (fresh) { syncRegistryFromBoardDb(fresh, db); setSessionBoard(fresh); }
121
+ return;
122
+ }
123
+
124
+ // 3. Walk up for a git root; fall back to cwd for non-git projects.
125
+ const gitRoot = walkUp(start, (dir) => existsSync(join(dir, ".git")));
126
+ const result = provisionRepoBoard(normalizePath(gitRoot || start));
127
+ if (result.registryRow) setSessionBoard(result.registryRow);
128
+ }
129
+
130
+ function createBoardAtRoot(root) {
131
+ const dbPath = join(root, ".orbit", "board.db");
132
+ mkdirSync(dirname(dbPath), { recursive: true });
133
+ const db = openConnection(dbPath);
134
+ createBoardSchema(db);
135
+ const seeded = seedIfEmpty(db, root);
136
+ if (seeded) {
137
+ const t = now();
138
+ insertBoard({ id: seeded.id, slug: seeded.slug, name: seeded.name, repo_path: root, db_path: dbPath, repo_url: seeded.repo_url || "", default_branch: seeded.default_branch || "main", last_active_at: t, created_at: t, updated_at: t });
139
+ }
140
+ const fresh = getBoardByRepoPath(root);
141
+ if (fresh) { syncRegistryFromBoardDb(fresh, db); setSessionBoard(fresh); }
142
+ }
143
+
144
+ initMcpSessionBoard();
145
+ closeAllConnections();
146
+
147
+ /** Build a ctx for the session board (or a board the tool argument selected). */
148
+ function ctxFor(boardRow, actor) {
149
+ if (!boardRow) throw rpcError(-32602, "No board in session; pass board_id or board_slug, or call board_set_active first.");
150
+ return { actor, board: boardRow, db: openBoardDb(boardRow) };
151
+ }
152
+
153
+ function sessionCtx(actor) {
154
+ return ctxFor(getSessionBoard(), actor);
155
+ }
156
+
157
+ function withAutomaticBackup(ctx, result) {
158
+ scheduleAutomaticBoardBackup(ctx.board, ctx.db);
159
+ return result;
160
+ }
161
+
162
+ /** Resolve a registry row from explicit args (no session fallback). */
163
+ function resolveExplicitBoard(args) {
164
+ if (args.board_id) {
165
+ const row = getBoardByRegistryId(args.board_id);
166
+ if (!row) throw rpcError(-32004, "Board id not found.");
167
+ return row;
168
+ }
169
+ if (args.board_slug || args.board) {
170
+ const row = getBoardBySlug(args.board_slug || args.board);
171
+ if (!row) throw rpcError(-32004, "Board slug not found.");
172
+ return row;
173
+ }
174
+ return null;
175
+ }
176
+
177
+ /** Resolve a board for a tool that accepts board_id/board_slug AND can fall
178
+ * back to the session board. */
179
+ function resolveBoardOrSession(args) {
180
+ return resolveExplicitBoard(args) || getSessionBoard();
181
+ }
15
182
 
16
183
  const TOOL_DEFS = [
17
184
  {
@@ -75,7 +242,7 @@ const TOOL_DEFS = [
75
242
  {
76
243
  name: "board_get_ticket_context",
77
244
  description:
78
- "Read the full context pack for a ticket, including board agent_instructions (project-level agent context), relations, blockers, comments, and child cards. Heavy — prefer board_read_ticket when you only need title/description/comments.",
245
+ "Read the default agent context pack for a ticket, including board agent_instructions (project-level agent context), relations, blockers, implementation fields, and child cards. Ticket comments are intentionally omitted; use board_read_comments for explicit comment retrieval.",
79
246
  inputSchema: {
80
247
  type: "object",
81
248
  properties: {
@@ -90,7 +257,7 @@ const TOOL_DEFS = [
90
257
  {
91
258
  name: "board_read_ticket",
92
259
  description:
93
- "Look up a ticket by ticket_id, number, or exact title (case-insensitive) on the session's active board, and return its title, description, labels, state, and full comment thread plus the board manual (agent_instructions + journal + deployment paths).",
260
+ "Look up a ticket by ticket_id, number, or exact title (case-insensitive) on the session's active board, and return its title, description, labels, state, and the board manual (agent_instructions + journal + deployment paths). Ticket comments are intentionally omitted; use board_read_comments for explicit comment retrieval.",
94
261
  inputSchema: {
95
262
  type: "object",
96
263
  properties: {
@@ -332,7 +499,9 @@ const TOOL_DEFS = [
332
499
  type: "object",
333
500
  properties: {
334
501
  board_id: { type: "string" },
335
- board_slug: { type: "string" }
502
+ board_slug: { type: "string" },
503
+ include_attachments: { type: "boolean", description: "Embed attached image bytes in the JSON snapshot." },
504
+ include_images: { type: "boolean", description: "Alias for include_attachments." }
336
505
  },
337
506
  additionalProperties: false
338
507
  },
@@ -341,7 +510,7 @@ const TOOL_DEFS = [
341
510
  {
342
511
  name: "board_update_settings",
343
512
  description:
344
- "PATCH-equivalent for board settings: name, repo_url, system_path, default_branch, agent_instructions (project-level agent context), and project_notes (Notes For You). Same fields as PATCH /api/boards/:id.",
513
+ "PATCH-equivalent for board settings: name (display rename only; slug/canonical URLs stay unchanged), repo_url, system_path, default_branch, agent_instructions (project-level agent context), and project_notes (Notes For You).",
345
514
  inputSchema: {
346
515
  type: "object",
347
516
  properties: {
@@ -364,13 +533,21 @@ const tools = new Map(TOOL_DEFS.map((tool) => [tool.name, tool]));
364
533
 
365
534
  let inputBuffer = Buffer.alloc(0);
366
535
  let expectedBodyLength = -1;
536
+ let shuttingDown = false;
537
+
538
+ function shutdown(code = 0) {
539
+ if (shuttingDown) return;
540
+ shuttingDown = true;
541
+ orbitClient.close?.();
542
+ process.exit(code);
543
+ }
367
544
 
368
545
  process.stdin.on("data", (chunk) => {
369
546
  inputBuffer = Buffer.concat([inputBuffer, chunk]);
370
547
  drainInput();
371
548
  });
372
549
 
373
- process.stdin.on("end", () => process.exit(0));
550
+ process.stdin.on("end", () => shutdown(0));
374
551
  process.stdin.resume();
375
552
 
376
553
  function drainInput() {
@@ -449,6 +626,8 @@ async function handleMessage(body, transport = "content-length") {
449
626
  }, transport);
450
627
  } catch (error) {
451
628
  writeMessage(errorResponse(message.id, error.code || -32000, error.message || "Internal error."), transport);
629
+ } finally {
630
+ orbitClient.close?.();
452
631
  }
453
632
  }
454
633
 
@@ -544,6 +723,11 @@ process.on("unhandledRejection", (error) => {
544
723
  writeMessage(errorResponse(null, -32000, error?.message || "Unhandled rejection."));
545
724
  });
546
725
 
726
+ process.once("SIGINT", () => shutdown(0));
727
+ process.once("SIGTERM", () => shutdown(0));
728
+ process.once("exit", () => orbitClient.close?.());
729
+
547
730
  if (process.env.MAB_MCP_STDERR_LOG === "1") {
548
- console.error(`Starscape Orbit MCP ready. Mode: ${orbitClient.mode}. Target: ${typeof orbitClient.sessionLabel === "function" ? orbitClient.sessionLabel() : orbitClient.sessionLabel}`);
731
+ const target = typeof orbitClient.sessionLabel === "function" ? orbitClient.sessionLabel() : orbitClient.sessionLabel;
732
+ console.error(`Starscape Orbit MCP ready. Mode: ${orbitClient.mode}. Target: ${target}`);
549
733
  }
package/src/server.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  startSSEStream,
9
9
  startupSummary
10
10
  } from "./core/board.js";
11
+ import { closeAllConnections } from "./core/db.js";
11
12
 
12
13
  const server = createServer(async (req, res) => {
13
14
  try {
@@ -46,3 +47,22 @@ server.listen(boardRuntime.port, () => {
46
47
  console.log(`Boards (${summary.boardCount}):`);
47
48
  for (const path of summary.boardPaths) console.log(` - ${path}`);
48
49
  });
50
+
51
+ let shuttingDown = false;
52
+
53
+ function shutdown() {
54
+ if (shuttingDown) return;
55
+ shuttingDown = true;
56
+ server.close(() => {
57
+ closeAllConnections();
58
+ process.exit(0);
59
+ });
60
+ setTimeout(() => {
61
+ closeAllConnections();
62
+ process.exit(0);
63
+ }, 1500).unref();
64
+ }
65
+
66
+ process.once("SIGINT", shutdown);
67
+ process.once("SIGTERM", shutdown);
68
+ process.once("exit", () => closeAllConnections());