@starlove/orbit 1.0.6 → 1.0.7

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.
@@ -0,0 +1,180 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+
4
+ function rpcError(code, message) {
5
+ const error = new Error(message);
6
+ error.code = code;
7
+ return error;
8
+ }
9
+
10
+ export function orbitMode(env = process.env) {
11
+ const mode = String(env.ORBIT_MODE || "local").trim().toLowerCase();
12
+ if (mode === "remote" || mode === "http") return "remote";
13
+ if (mode === "local" || mode === "") return "local";
14
+ throw rpcError(-32602, `Unsupported ORBIT_MODE: ${env.ORBIT_MODE}`);
15
+ }
16
+
17
+ export async function createOrbitClient(env = process.env) {
18
+ return orbitMode(env) === "remote" ? createHttpOrbitClient(env) : createLocalOrbitClient(env);
19
+ }
20
+
21
+ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.fetch) {
22
+ const apiUrl = String(env.ORBIT_API_URL || "").trim().replace(/\/+$/, "");
23
+ const defaultBoard = String(env.ORBIT_DEFAULT_BOARD || "").trim();
24
+ if (!apiUrl) throw rpcError(-32602, "ORBIT_MODE=remote requires ORBIT_API_URL.");
25
+ if (typeof fetchImpl !== "function") throw rpcError(-32603, "ORBIT_MODE=remote requires global fetch support.");
26
+
27
+ async function request(method, path, { query = {}, body, ok = [200] } = {}) {
28
+ const url = new URL(`${apiUrl}${path}`);
29
+ for (const [key, value] of Object.entries(query)) {
30
+ if (value !== undefined && value !== null && value !== "") url.searchParams.set(key, String(value));
31
+ }
32
+ const response = await fetchImpl(url, {
33
+ method,
34
+ headers: body === undefined ? undefined : { "Content-Type": "application/json" },
35
+ body: body === undefined ? undefined : JSON.stringify(body)
36
+ });
37
+ let payload = null;
38
+ const text = await response.text();
39
+ if (text) {
40
+ try { payload = JSON.parse(text); } catch { payload = { message: text }; }
41
+ }
42
+ if (!ok.includes(response.status)) {
43
+ const message = payload?.message || payload?.error || `Orbit API ${method} ${url.pathname} failed with HTTP ${response.status}`;
44
+ throw rpcError(-32000, message);
45
+ }
46
+ return payload ?? {};
47
+ }
48
+
49
+ function boardArg(args = {}) {
50
+ return args.board_id || args.board_slug || args.board || defaultBoard;
51
+ }
52
+
53
+ function requireBoard(args = {}) {
54
+ const board = boardArg(args);
55
+ if (!board) throw rpcError(-32602, "Remote Orbit MCP requires board_id/board_slug/board or ORBIT_DEFAULT_BOARD.");
56
+ return board;
57
+ }
58
+
59
+ const ticketQuery = (args = {}) => ({ board_id: args.board_id, board: args.board_slug || args.board || defaultBoard });
60
+
61
+ return {
62
+ mode: "remote",
63
+ sessionLabel: apiUrl,
64
+ async boardContext(args = {}) {
65
+ return request("GET", `/api/boards/${encodeURIComponent(requireBoard(args))}/context`, { query: { include_struck: args.include_struck ? "true" : undefined } });
66
+ },
67
+ async boardList() { return request("GET", "/api/boards"); },
68
+ async boardSetActive(args = {}) { if (!args.slug) throw rpcError(-32602, "slug is required."); env.ORBIT_DEFAULT_BOARD = String(args.slug); return { ok: true, slug: String(args.slug), mode: "remote" }; },
69
+ async claimNext(args = {}) { return request("POST", "/api/agent/claim-next", { body: { ...args, board: requireBoard(args) } }); },
70
+ async getTicketContext(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/context`, { query: { ...ticketQuery(args), depth: args.depth || 1 } }); },
71
+ async readTicket(args = {}) {
72
+ if (args.ticket_id) return this.getTicketContext({ ...args, depth: 1 });
73
+ const found = await this.search({ q: args.title || String(args.number || ""), limit: 10, ...args });
74
+ return found;
75
+ },
76
+ async readComments(args = {}) { const ctx = await this.readTicket(args); return { comments: ctx.comments || ctx.ticket?.comments || [] }; },
77
+ async getTicketRelations(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/relations`, { query: ticketQuery(args) }); },
78
+ async getTicketBlockers(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/blockers`, { query: ticketQuery(args) }); },
79
+ async search(args = {}) { return request("GET", "/api/search", { query: { q: args.q, limit: args.limit, board_id: args.board_id, board: args.board_slug || args.board || defaultBoard } }); },
80
+ async createTicket(args = {}) { return request("POST", "/api/tickets", { body: { ...args, board: requireBoard(args) }, ok: [201] }); },
81
+ async updateTicket({ ticket_id, ...patch }) { return request("PATCH", `/api/tickets/${encodeURIComponent(ticket_id)}`, { query: ticketQuery(patch), body: patch }); },
82
+ async addComment({ ticket_id, ...body }) { return request("POST", `/api/tickets/${encodeURIComponent(ticket_id)}/comments`, { body, ok: [201] }); },
83
+ async addBoardEntry(args = {}) { return request("POST", `/api/boards/${encodeURIComponent(requireBoard(args))}/entries`, { body: args, ok: [201] }); },
84
+ async checkpoint(args = {}) { return request("POST", "/api/agent/checkpoint", { body: args }); },
85
+ async complete(args = {}) { return request("POST", "/api/agent/complete", { body: args }); },
86
+ async archiveTicket(args = {}) { return request("POST", `/api/tickets/${encodeURIComponent(args.ticket_id)}/archive`, { query: ticketQuery(args) }); },
87
+ async restoreTicket(args = {}) { return request("POST", `/api/tickets/${encodeURIComponent(args.ticket_id)}/restore`, { query: ticketQuery(args) }); },
88
+ async deleteTicket(args = {}) { return request("DELETE", `/api/tickets/${encodeURIComponent(args.ticket_id)}`, { query: ticketQuery(args) }); },
89
+ async listArchive(args = {}) { return request("GET", `/api/boards/${encodeURIComponent(requireBoard(args))}/archive`); },
90
+ async exportBoard(args = {}) { return request("GET", `/api/boards/${encodeURIComponent(requireBoard(args))}/export`); },
91
+ async updateSettings(args = {}) { const { board_id, board_slug, board, ...patch } = args; return request("PATCH", `/api/boards/${encodeURIComponent(requireBoard(args))}`, { body: patch }); }
92
+ };
93
+ }
94
+
95
+ export async function createLocalOrbitClient(env = process.env) {
96
+ const board = await import("../core/board.js");
97
+ const dbMod = await import("../core/db.js");
98
+ const registry = await import("../core/registry.js");
99
+ const backups = await import("../core/backups.js");
100
+ const seed = await import("../core/seed.js");
101
+ const util = await import("../core/util.js");
102
+
103
+ let sessionBoardRow = null;
104
+ const setSessionBoard = (row) => { sessionBoardRow = row; if (row) registry.touchBoardActive(row.id); };
105
+ const getSessionBoard = () => sessionBoardRow;
106
+ const walkUp = (startDir, predicate) => { let dir = startDir; while (true) { if (predicate(dir)) return dir; const parent = dirname(dir); if (parent === dir) return null; dir = parent; } };
107
+ const createBoardAtRoot = (root) => {
108
+ const dbPath = join(root, ".orbit", "board.db");
109
+ mkdirSync(dirname(dbPath), { recursive: true });
110
+ const db = dbMod.openConnection(dbPath);
111
+ dbMod.createBoardSchema(db);
112
+ const seeded = seed.seedIfEmpty(db, root);
113
+ if (seeded) {
114
+ const t = util.now();
115
+ registry.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 });
116
+ }
117
+ const fresh = registry.getBoardByRepoPath(root);
118
+ if (fresh) { registry.syncRegistryFromBoardDb(fresh, db); setSessionBoard(fresh); }
119
+ };
120
+
121
+ const start = util.normalizePath(env.PROJECT_ROOT ? resolve(env.PROJECT_ROOT) : process.cwd());
122
+ const boardRoot = walkUp(start, (dir) => existsSync(join(dir, ".orbit", "board.db")));
123
+ if (boardRoot) {
124
+ const root = util.normalizePath(boardRoot);
125
+ const existing = registry.getBoardByRepoPath(root);
126
+ if (existing) setSessionBoard(existing);
127
+ else {
128
+ const dbPath = join(boardRoot, ".orbit", "board.db");
129
+ const db = dbMod.openConnection(dbPath);
130
+ dbMod.createBoardSchema(db);
131
+ const seeded = seed.seedIfEmpty(db, root);
132
+ if (seeded) {
133
+ const t = util.now();
134
+ registry.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 });
135
+ }
136
+ const fresh = registry.getBoardByRepoPath(root);
137
+ if (fresh) { registry.syncRegistryFromBoardDb(fresh, db); setSessionBoard(fresh); }
138
+ }
139
+ } else {
140
+ const gitRoot = walkUp(start, (dir) => existsSync(join(dir, ".git")));
141
+ createBoardAtRoot(util.normalizePath(gitRoot || start));
142
+ }
143
+
144
+ const ctxFor = (row, actor) => {
145
+ if (!row) throw rpcError(-32602, "No board in session; pass board_id or board_slug, or call board_set_active first.");
146
+ return { actor, board: row, db: registry.openBoardDb(row) };
147
+ };
148
+ const sessionCtx = (actor) => ctxFor(getSessionBoard(), actor);
149
+ const backup = (ctx, result) => { backups.scheduleAutomaticBoardBackup(ctx.board, ctx.db); return result; };
150
+ const explicit = (args = {}) => args.board_id ? registry.getBoardByRegistryId(args.board_id) : (args.board_slug || args.board ? registry.getBoardBySlug(args.board_slug || args.board) : null);
151
+ const rowOrSession = (args = {}) => explicit(args) || getSessionBoard();
152
+ const actor = () => board.localAgentActor();
153
+
154
+ return {
155
+ mode: "local",
156
+ sessionLabel: () => getSessionBoard()?.db_path || "(none)",
157
+ boardContext: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getBoardContext(ctx.board.id, ctx, { includeStruck: Boolean(args.include_struck) }); },
158
+ boardList: () => ({ boards: registry.listBoards().map((row) => ({ id: row.id, slug: row.slug, name: row.name, repo_path: row.repo_path })) }),
159
+ boardSetActive: (args = {}) => { const slug = String(args.slug || "").trim(); if (!slug) throw rpcError(-32602, "slug is required."); const row = registry.getBoardBySlug(slug); if (!row) throw rpcError(-32004, `Board slug not found: ${slug}`); setSessionBoard(row); return { ok: true, board_id: row.id, slug: row.slug, name: row.name, db_path: row.db_path }; },
160
+ claimNext: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.claimNext(args, ctx)); },
161
+ getTicketContext: (args = {}) => board.getContextPack(args.ticket_id, sessionCtx(actor()), Number(args.depth || 1)),
162
+ readTicket: (args = {}) => board.readTicket(args, sessionCtx(actor())),
163
+ readComments: (args = {}) => board.readComments(args, sessionCtx(actor())),
164
+ getTicketRelations: (args = {}) => board.getTicketRelations(args.ticket_id, sessionCtx(actor())),
165
+ getTicketBlockers: (args = {}) => board.getTicketBlockers(args.ticket_id, sessionCtx(actor())),
166
+ search: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.searchTickets({ q: args.q, limit: args.limit }, ctx); },
167
+ createTicket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); const { board_slug, board: _b, ...body } = args; body.board_id = ctx.board.id; return backup(ctx, board.createTicket(body, ctx)); },
168
+ updateTicket: ({ ticket_id, ...patch }) => { const ctx = sessionCtx(actor()); return backup(ctx, board.updateTicket(ticket_id, patch, ctx)); },
169
+ addComment: ({ ticket_id, ...body }) => { const ctx = sessionCtx(actor()); return backup(ctx, board.createComment(ticket_id, body, ctx)); },
170
+ addBoardEntry: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.createBoardEntry(ctx.board.id, args, ctx)); },
171
+ checkpoint: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.checkpointTicket(args, ctx)); },
172
+ complete: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.completeTicket(args, ctx)); },
173
+ archiveTicket: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.archiveTicket(args.ticket_id, ctx)); },
174
+ restoreTicket: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.restoreTicket(args.ticket_id, ctx)); },
175
+ deleteTicket: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.deleteTicket(args.ticket_id, ctx)); },
176
+ listArchive: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return { tickets: board.archivedTicketsForBoard(ctx.db, ctx.board.id) }; },
177
+ exportBoard: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.exportBoard(ctx.board.id, ctx); },
178
+ updateSettings: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); const { board_id, board_slug, ...patch } = args; return backup(ctx, board.updateBoard(ctx.board.id, patch, ctx)); }
179
+ };
180
+ }
package/src/mcp-server.js CHANGED
@@ -4,166 +4,14 @@
4
4
  // the call it's about to make, so adding new tools never has to think about
5
5
  // "active board" state.
6
6
 
7
- import { existsSync, mkdirSync } from "node:fs";
8
- import { dirname, join, resolve } from "node:path";
9
- import {
10
- archivedTicketsForBoard,
11
- archiveTicket,
12
- checkpointTicket,
13
- claimNext,
14
- completeTicket,
15
- createBoardEntry,
16
- createComment,
17
- deleteTicket,
18
- exportBoard,
19
- getBoardContext,
20
- getContextPack,
21
- readComments,
22
- readTicket,
23
- getTicketBlockers,
24
- getTicketRelations,
25
- localAgentActor,
26
- restoreTicket,
27
- searchTickets,
28
- updateBoard,
29
- updateTicket
30
- } from "./core/board.js";
31
- import { createBoardSchema, openConnection } from "./core/db.js";
32
- import {
33
- getBoardByRegistryId,
34
- getBoardBySlug,
35
- getBoardByRepoPath,
36
- insertBoard,
37
- listBoards,
38
- openBoardDb,
39
- syncRegistryFromBoardDb,
40
- touchBoardActive
41
- } from "./core/registry.js";
42
- import { scheduleAutomaticBoardBackup } from "./core/backups.js";
43
- import { seedIfEmpty } from "./core/seed.js";
44
- import { now, normalizePath } from "./core/util.js";
7
+ import { createOrbitClient } from "./mcp/orbit-client.js";
45
8
 
46
9
  const SERVER_INFO = {
47
10
  name: "minimal-agent-board",
48
11
  version: "0.1.0"
49
12
  };
50
13
 
51
- // Per-process session board. Set on init by walking up from cwd, mutable via
52
- // `board_set_active`. NEVER read by anything outside this file — every
53
- // downstream call receives an explicit ctx.
54
- let sessionBoardRow = null;
55
-
56
- function setSessionBoard(row) {
57
- sessionBoardRow = row;
58
- if (row) touchBoardActive(row.id);
59
- }
60
-
61
- function getSessionBoard() {
62
- return sessionBoardRow;
63
- }
64
-
65
- /**
66
- * Walk up from `startDir` toward the filesystem root, returning the first
67
- * directory that satisfies `predicate`. Returns null if none found.
68
- */
69
- function walkUp(startDir, predicate) {
70
- let dir = startDir;
71
- while (true) {
72
- if (predicate(dir)) return dir;
73
- const parent = dirname(dir);
74
- if (parent === dir) return null;
75
- dir = parent;
76
- }
77
- }
78
-
79
- /**
80
- * Initialise the MCP session board. Resolution order:
81
- * 1. Walk up from PROJECT_ROOT (cwd unless `orbit mcp --cwd` or env overrides it)
82
- * for an existing `.orbit/board.db` — finds the board even when the agent
83
- * is launched from a subdirectory or MCP config uses an absolute command.
84
- * 2. Walk up from PROJECT_ROOT for a `.git/` directory — auto-creates a board at
85
- * the git root on first launch.
86
- * 3. Fall back to PROJECT_ROOT itself — supports non-git projects and global installs.
87
- */
88
- function initMcpSessionBoard() {
89
- const start = normalizePath(process.env.PROJECT_ROOT ? resolve(process.env.PROJECT_ROOT) : process.cwd());
90
-
91
- // 1. Walk up for an existing board db.
92
- const boardRoot = walkUp(start, (dir) => existsSync(join(dir, ".orbit", "board.db")));
93
- if (boardRoot) {
94
- const root = normalizePath(boardRoot);
95
- const existing = getBoardByRepoPath(root);
96
- if (existing) { setSessionBoard(existing); return; }
97
- // db exists on disk but isn't in the registry yet — register it.
98
- const dbPath = join(boardRoot, ".orbit", "board.db");
99
- const db = openConnection(dbPath);
100
- createBoardSchema(db);
101
- const seeded = seedIfEmpty(db, root);
102
- if (seeded) {
103
- const t = now();
104
- 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 });
105
- }
106
- const fresh = getBoardByRepoPath(root);
107
- if (fresh) { syncRegistryFromBoardDb(fresh, db); setSessionBoard(fresh); }
108
- return;
109
- }
110
-
111
- // 2. Walk up for a git root; fall back to cwd for non-git projects.
112
- const gitRoot = walkUp(start, (dir) => existsSync(join(dir, ".git")));
113
- createBoardAtRoot(normalizePath(gitRoot || start));
114
- }
115
-
116
- function createBoardAtRoot(root) {
117
- const dbPath = join(root, ".orbit", "board.db");
118
- mkdirSync(dirname(dbPath), { recursive: true });
119
- const db = openConnection(dbPath);
120
- createBoardSchema(db);
121
- const seeded = seedIfEmpty(db, root);
122
- if (seeded) {
123
- const t = now();
124
- 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 });
125
- }
126
- const fresh = getBoardByRepoPath(root);
127
- if (fresh) { syncRegistryFromBoardDb(fresh, db); setSessionBoard(fresh); }
128
- }
129
-
130
- initMcpSessionBoard();
131
-
132
- /** Build a ctx for the session board (or a board the tool argument selected). */
133
- function ctxFor(boardRow, actor) {
134
- if (!boardRow) throw rpcError(-32602, "No board in session; pass board_id or board_slug, or call board_set_active first.");
135
- return { actor, board: boardRow, db: openBoardDb(boardRow) };
136
- }
137
-
138
- function sessionCtx(actor) {
139
- return ctxFor(getSessionBoard(), actor);
140
- }
141
-
142
- function withAutomaticBackup(ctx, result) {
143
- scheduleAutomaticBoardBackup(ctx.board, ctx.db);
144
- return result;
145
- }
146
-
147
- /** Resolve a registry row from explicit args (no session fallback). */
148
- function resolveExplicitBoard(args) {
149
- if (args.board_id) {
150
- const row = getBoardByRegistryId(args.board_id);
151
- if (!row) throw rpcError(-32004, "Board id not found.");
152
- return row;
153
- }
154
- if (args.board_slug || args.board) {
155
- const row = getBoardBySlug(args.board_slug || args.board);
156
- if (!row) throw rpcError(-32004, "Board slug not found.");
157
- return row;
158
- }
159
- return null;
160
- }
161
-
162
- /** Resolve a board for a tool that accepts board_id/board_slug AND can fall
163
- * back to the session board. */
164
- function resolveBoardOrSession(args) {
165
- return resolveExplicitBoard(args) || getSessionBoard();
166
- }
14
+ const orbitClient = await createOrbitClient();
167
15
 
168
16
  const TOOL_DEFS = [
169
17
  {
@@ -180,12 +28,7 @@ const TOOL_DEFS = [
180
28
  },
181
29
  additionalProperties: false
182
30
  },
183
- handler: (args) => {
184
- const actor = localAgentActor();
185
- const row = resolveBoardOrSession(args);
186
- const ctx = ctxFor(row, actor);
187
- return getBoardContext(ctx.board.id, ctx, { includeStruck: Boolean(args.include_struck) });
188
- }
31
+ handler: (args) => orbitClient.boardContext(args)
189
32
  },
190
33
  {
191
34
  name: "board_list",
@@ -196,16 +39,7 @@ const TOOL_DEFS = [
196
39
  properties: {},
197
40
  additionalProperties: false
198
41
  },
199
- handler: () => {
200
- return {
201
- boards: listBoards().map((row) => ({
202
- id: row.id,
203
- slug: row.slug,
204
- name: row.name,
205
- repo_path: row.repo_path
206
- }))
207
- };
208
- }
42
+ handler: () => orbitClient.boardList()
209
43
  },
210
44
  {
211
45
  name: "board_set_active",
@@ -219,14 +53,7 @@ const TOOL_DEFS = [
219
53
  required: ["slug"],
220
54
  additionalProperties: false
221
55
  },
222
- handler: (args) => {
223
- const slug = String(args.slug || "").trim();
224
- if (!slug) throw rpcError(-32602, "slug is required.");
225
- const row = getBoardBySlug(slug);
226
- if (!row) throw rpcError(-32004, `Board slug not found: ${slug}`);
227
- setSessionBoard(row);
228
- return { ok: true, board_id: row.id, slug: row.slug, name: row.name, db_path: row.db_path };
229
- }
56
+ handler: (args) => orbitClient.boardSetActive(args)
230
57
  },
231
58
  {
232
59
  name: "board_claim_next",
@@ -243,12 +70,7 @@ const TOOL_DEFS = [
243
70
  },
244
71
  additionalProperties: false
245
72
  },
246
- handler: (args) => {
247
- const actor = localAgentActor();
248
- const row = resolveBoardOrSession(args);
249
- const ctx = ctxFor(row, actor);
250
- return withAutomaticBackup(ctx, claimNext(args, ctx));
251
- }
73
+ handler: (args) => orbitClient.claimNext(args)
252
74
  },
253
75
  {
254
76
  name: "board_get_ticket_context",
@@ -263,11 +85,7 @@ const TOOL_DEFS = [
263
85
  required: ["ticket_id"],
264
86
  additionalProperties: false
265
87
  },
266
- handler: (args) => {
267
- const actor = localAgentActor();
268
- const ctx = sessionCtx(actor);
269
- return getContextPack(args.ticket_id, ctx, Number(args.depth || 1));
270
- }
88
+ handler: (args) => orbitClient.getTicketContext(args)
271
89
  },
272
90
  {
273
91
  name: "board_read_ticket",
@@ -282,11 +100,7 @@ const TOOL_DEFS = [
282
100
  },
283
101
  additionalProperties: false
284
102
  },
285
- handler: (args) => {
286
- const actor = localAgentActor();
287
- const ctx = sessionCtx(actor);
288
- return readTicket(args, ctx);
289
- }
103
+ handler: (args) => orbitClient.readTicket(args)
290
104
  },
291
105
  {
292
106
  name: "board_read_comments",
@@ -301,11 +115,7 @@ const TOOL_DEFS = [
301
115
  },
302
116
  additionalProperties: false
303
117
  },
304
- handler: (args) => {
305
- const actor = localAgentActor();
306
- const ctx = sessionCtx(actor);
307
- return readComments(args, ctx);
308
- }
118
+ handler: (args) => orbitClient.readComments(args)
309
119
  },
310
120
  {
311
121
  name: "board_get_ticket_relations",
@@ -318,10 +128,7 @@ const TOOL_DEFS = [
318
128
  required: ["ticket_id"],
319
129
  additionalProperties: false
320
130
  },
321
- handler: (args) => {
322
- const actor = localAgentActor();
323
- return getTicketRelations(args.ticket_id, sessionCtx(actor));
324
- }
131
+ handler: (args) => orbitClient.getTicketRelations(args)
325
132
  },
326
133
  {
327
134
  name: "board_get_ticket_blockers",
@@ -334,10 +141,7 @@ const TOOL_DEFS = [
334
141
  required: ["ticket_id"],
335
142
  additionalProperties: false
336
143
  },
337
- handler: (args) => {
338
- const actor = localAgentActor();
339
- return getTicketBlockers(args.ticket_id, sessionCtx(actor));
340
- }
144
+ handler: (args) => orbitClient.getTicketBlockers(args)
341
145
  },
342
146
  {
343
147
  name: "board_search",
@@ -355,12 +159,33 @@ const TOOL_DEFS = [
355
159
  required: ["q"],
356
160
  additionalProperties: false
357
161
  },
358
- handler: (args) => {
359
- const actor = localAgentActor();
360
- const row = resolveBoardOrSession(args);
361
- const ctx = ctxFor(row, actor);
362
- return searchTickets({ q: args.q, limit: args.limit }, ctx);
363
- }
162
+ handler: (args) => orbitClient.search(args)
163
+ },
164
+ {
165
+ name: "board_create_ticket",
166
+ description:
167
+ "Create a new Orbit ticket/card on the active board. Use this instead of editing .orbit/board.db directly. Same operation as POST /api/tickets.",
168
+ inputSchema: {
169
+ type: "object",
170
+ properties: {
171
+ board_id: { type: "string" },
172
+ board_slug: { type: "string" },
173
+ board: { type: "string" },
174
+ title: { type: "string" },
175
+ description: { type: "string" },
176
+ type: { type: "string", enum: ["epic", "feature", "task", "bug"] },
177
+ parent_ticket_id: { type: ["string", "null"] },
178
+ ai_plan: { type: "string" },
179
+ implementation_summary: { type: "string" },
180
+ implementation_updates: { type: "string" },
181
+ state_id: { type: "string" },
182
+ priority: { type: "integer" },
183
+ labels: { type: "array", items: { type: "string" } }
184
+ },
185
+ required: ["title"],
186
+ additionalProperties: false
187
+ },
188
+ handler: (args) => orbitClient.createTicket(args)
364
189
  },
365
190
  {
366
191
  name: "board_update_ticket",
@@ -384,11 +209,7 @@ const TOOL_DEFS = [
384
209
  required: ["ticket_id"],
385
210
  additionalProperties: false
386
211
  },
387
- handler: ({ ticket_id, ...patch }) => {
388
- const actor = localAgentActor();
389
- const ctx = sessionCtx(actor);
390
- return withAutomaticBackup(ctx, updateTicket(ticket_id, patch, ctx));
391
- }
212
+ handler: ({ ticket_id, ...patch }) => orbitClient.updateTicket({ ticket_id, ...patch })
392
213
  },
393
214
  {
394
215
  name: "board_add_comment",
@@ -403,11 +224,7 @@ const TOOL_DEFS = [
403
224
  required: ["ticket_id", "body"],
404
225
  additionalProperties: false
405
226
  },
406
- handler: ({ ticket_id, ...body }) => {
407
- const actor = localAgentActor();
408
- const ctx = sessionCtx(actor);
409
- return withAutomaticBackup(ctx, createComment(ticket_id, body, ctx));
410
- }
227
+ handler: ({ ticket_id, ...body }) => orbitClient.addComment({ ticket_id, ...body })
411
228
  },
412
229
  {
413
230
  name: "board_add_board_entry",
@@ -425,12 +242,7 @@ const TOOL_DEFS = [
425
242
  required: ["type", "title"],
426
243
  additionalProperties: false
427
244
  },
428
- handler: (args) => {
429
- const actor = localAgentActor();
430
- const row = resolveBoardOrSession(args);
431
- const ctx = ctxFor(row, actor);
432
- return withAutomaticBackup(ctx, createBoardEntry(ctx.board.id, args, ctx));
433
- }
245
+ handler: (args) => orbitClient.addBoardEntry(args)
434
246
  },
435
247
  {
436
248
  name: "board_checkpoint",
@@ -445,11 +257,7 @@ const TOOL_DEFS = [
445
257
  required: ["ticket_id", "message"],
446
258
  additionalProperties: false
447
259
  },
448
- handler: (args) => {
449
- const actor = localAgentActor();
450
- const ctx = sessionCtx(actor);
451
- return withAutomaticBackup(ctx, checkpointTicket(args, ctx));
452
- }
260
+ handler: (args) => orbitClient.checkpoint(args)
453
261
  },
454
262
  {
455
263
  name: "board_complete",
@@ -467,11 +275,7 @@ const TOOL_DEFS = [
467
275
  required: ["ticket_id", "summary"],
468
276
  additionalProperties: false
469
277
  },
470
- handler: (args) => {
471
- const actor = localAgentActor();
472
- const ctx = sessionCtx(actor);
473
- return withAutomaticBackup(ctx, completeTicket(args, ctx));
474
- }
278
+ handler: (args) => orbitClient.complete(args)
475
279
  },
476
280
  {
477
281
  name: "board_archive_ticket",
@@ -483,11 +287,7 @@ const TOOL_DEFS = [
483
287
  required: ["ticket_id"],
484
288
  additionalProperties: false
485
289
  },
486
- handler: (args) => {
487
- const actor = localAgentActor();
488
- const ctx = sessionCtx(actor);
489
- return withAutomaticBackup(ctx, archiveTicket(args.ticket_id, ctx));
490
- }
290
+ handler: (args) => orbitClient.archiveTicket(args)
491
291
  },
492
292
  {
493
293
  name: "board_restore_ticket",
@@ -498,11 +298,7 @@ const TOOL_DEFS = [
498
298
  required: ["ticket_id"],
499
299
  additionalProperties: false
500
300
  },
501
- handler: (args) => {
502
- const actor = localAgentActor();
503
- const ctx = sessionCtx(actor);
504
- return withAutomaticBackup(ctx, restoreTicket(args.ticket_id, ctx));
505
- }
301
+ handler: (args) => orbitClient.restoreTicket(args)
506
302
  },
507
303
  {
508
304
  name: "board_delete_ticket",
@@ -514,11 +310,7 @@ const TOOL_DEFS = [
514
310
  required: ["ticket_id"],
515
311
  additionalProperties: false
516
312
  },
517
- handler: (args) => {
518
- const actor = localAgentActor();
519
- const ctx = sessionCtx(actor);
520
- return withAutomaticBackup(ctx, deleteTicket(args.ticket_id, ctx));
521
- }
313
+ handler: (args) => orbitClient.deleteTicket(args)
522
314
  },
523
315
  {
524
316
  name: "board_list_archive",
@@ -531,12 +323,7 @@ const TOOL_DEFS = [
531
323
  },
532
324
  additionalProperties: false
533
325
  },
534
- handler: (args) => {
535
- const actor = localAgentActor();
536
- const row = resolveBoardOrSession(args);
537
- const ctx = ctxFor(row, actor);
538
- return { tickets: archivedTicketsForBoard(ctx.db, ctx.board.id) };
539
- }
326
+ handler: (args) => orbitClient.listArchive(args)
540
327
  },
541
328
  {
542
329
  name: "board_export_board",
@@ -549,12 +336,7 @@ const TOOL_DEFS = [
549
336
  },
550
337
  additionalProperties: false
551
338
  },
552
- handler: (args) => {
553
- const actor = localAgentActor();
554
- const row = resolveBoardOrSession(args);
555
- const ctx = ctxFor(row, actor);
556
- return exportBoard(ctx.board.id, ctx);
557
- }
339
+ handler: (args) => orbitClient.exportBoard(args)
558
340
  },
559
341
  {
560
342
  name: "board_update_settings",
@@ -574,13 +356,7 @@ const TOOL_DEFS = [
574
356
  },
575
357
  additionalProperties: false
576
358
  },
577
- handler: (args) => {
578
- const actor = localAgentActor();
579
- const row = resolveBoardOrSession(args);
580
- const ctx = ctxFor(row, actor);
581
- const { board_id, board_slug, ...patch } = args;
582
- return withAutomaticBackup(ctx, updateBoard(ctx.board.id, patch, ctx));
583
- }
359
+ handler: (args) => orbitClient.updateSettings(args)
584
360
  }
585
361
  ];
586
362
 
@@ -769,5 +545,5 @@ process.on("unhandledRejection", (error) => {
769
545
  });
770
546
 
771
547
  if (process.env.MAB_MCP_STDERR_LOG === "1") {
772
- console.error(`Starscape Orbit MCP ready. Session board: ${getSessionBoard()?.db_path || "(none)"}`);
548
+ console.error(`Starscape Orbit MCP ready. Mode: ${orbitClient.mode}. Target: ${typeof orbitClient.sessionLabel === "function" ? orbitClient.sessionLabel() : orbitClient.sessionLabel}`);
773
549
  }