@starlove/orbit 1.0.6 → 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.
- package/README.md +22 -7
- package/SKILL-ORBIT.md +28 -1
- package/build.mjs +2 -2
- package/dist/full/index.html +2 -2
- package/dist/full/js/app.a4f045fc.js +676 -0
- package/dist/full/js/app.a4f045fc.js.map +7 -0
- package/dist/full/{styles.c2e3d20a.css → styles.836872e7.css} +1117 -47
- package/dist/full/version.json +2 -2
- package/docs/AGENT_PROTOCOL.md +27 -5
- package/docs/MCP_SETUP.md +7 -3
- package/docs/ORBIT_DISPATCH.md +185 -0
- package/docs/setup/orbit-init-wizard.md +174 -0
- package/package.json +2 -2
- package/public/js/actor-labels.js +65 -0
- package/public/js/api.js +5 -1
- package/public/js/app.js +8 -1
- package/public/js/card-actions.js +526 -0
- package/public/js/config.js +2 -1
- package/public/js/drawer.js +2 -0
- package/public/js/format.js +336 -0
- package/public/js/kanban.js +25 -2
- package/public/js/local-backend.js +24 -8
- package/public/js/main.js +14 -6
- package/public/js/motion-preference.js +53 -0
- package/public/js/router.js +78 -58
- package/public/js/search.js +6 -3
- package/public/js/settings-tabs.js +1 -1
- package/public/js/settings.js +192 -29
- package/public/js/starscape.js +25 -18
- package/public/js/state.js +61 -2
- package/public/js/theme-preference.js +88 -0
- package/public/js/ticket-detail.js +432 -126
- package/public/js/url-routes.js +94 -0
- package/public/styles.css +1117 -47
- package/src/cli/orbit.js +217 -32
- package/src/core/agent.js +1 -18
- package/src/core/agents-md.js +16 -10
- package/src/core/attachments.js +238 -0
- package/src/core/auth.js +36 -0
- package/src/core/backups.js +9 -3
- package/src/core/board.js +32 -1
- package/src/core/boards.js +1 -1
- package/src/core/bootstrap.js +9 -1
- package/src/core/db.js +42 -2
- package/src/core/delete-board.js +87 -21
- package/src/core/dispatch.js +532 -0
- package/src/core/http.js +31 -11
- package/src/core/migrate.js +198 -0
- package/src/core/paths.js +6 -0
- package/src/core/registry.js +12 -9
- package/src/core/router.js +62 -6
- package/src/core/search.js +56 -22
- package/src/core/snapshots.js +14 -2
- package/src/mcp/orbit-client.js +223 -0
- package/src/mcp-server.js +89 -129
- package/src/server.js +20 -0
- package/dist/full/js/app.aedc1bfe.js +0 -554
- package/dist/full/js/app.aedc1bfe.js.map +0 -7
|
@@ -0,0 +1,223 @@
|
|
|
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
|
+
close() {},
|
|
65
|
+
async boardContext(args = {}) {
|
|
66
|
+
return request("GET", `/api/boards/${encodeURIComponent(requireBoard(args))}/context`, { query: { include_struck: args.include_struck ? "true" : undefined } });
|
|
67
|
+
},
|
|
68
|
+
async boardList() { return request("GET", "/api/boards"); },
|
|
69
|
+
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" }; },
|
|
70
|
+
async claimNext(args = {}) { return request("POST", "/api/agent/claim-next", { body: { ...args, board: requireBoard(args) } }); },
|
|
71
|
+
async getTicketContext(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/context`, { query: { ...ticketQuery(args), depth: args.depth || 1 } }); },
|
|
72
|
+
async readTicket(args = {}) {
|
|
73
|
+
if (args.ticket_id) {
|
|
74
|
+
return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}`, { query: ticketQuery(args) });
|
|
75
|
+
}
|
|
76
|
+
if (args.number !== undefined || args.title) {
|
|
77
|
+
return request("GET", "/api/tickets/lookup", { query: { ...ticketQuery(args), number: args.number, title: args.title } });
|
|
78
|
+
}
|
|
79
|
+
throw rpcError(-32602, "ticket_id_or_number_or_title_required");
|
|
80
|
+
},
|
|
81
|
+
async readComments(args = {}) {
|
|
82
|
+
if (args.ticket_id) {
|
|
83
|
+
return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/comments`, { query: ticketQuery(args) });
|
|
84
|
+
}
|
|
85
|
+
const found = await this.readTicket(args);
|
|
86
|
+
const ticket = found.ticket || found.results?.find((row) =>
|
|
87
|
+
(args.number !== undefined && Number(row.number) === Number(args.number)) ||
|
|
88
|
+
(args.title && String(row.title || "").toLowerCase() === String(args.title).toLowerCase())
|
|
89
|
+
);
|
|
90
|
+
if (!ticket?.id) throw rpcError(-32004, "ticket_not_found");
|
|
91
|
+
return request("GET", `/api/tickets/${encodeURIComponent(ticket.id)}/comments`, { query: ticketQuery(args) });
|
|
92
|
+
},
|
|
93
|
+
async getTicketRelations(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/relations`, { query: ticketQuery(args) }); },
|
|
94
|
+
async getTicketBlockers(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/blockers`, { query: ticketQuery(args) }); },
|
|
95
|
+
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 } }); },
|
|
96
|
+
async createTicket(args = {}) { return request("POST", "/api/tickets", { body: { ...args, board: requireBoard(args) }, ok: [201] }); },
|
|
97
|
+
async updateTicket({ ticket_id, ...patch }) { return request("PATCH", `/api/tickets/${encodeURIComponent(ticket_id)}`, { query: ticketQuery(patch), body: patch }); },
|
|
98
|
+
async addComment({ ticket_id, ...body }) { return request("POST", `/api/tickets/${encodeURIComponent(ticket_id)}/comments`, { body, ok: [201] }); },
|
|
99
|
+
async addBoardEntry(args = {}) { return request("POST", `/api/boards/${encodeURIComponent(requireBoard(args))}/entries`, { body: args, ok: [201] }); },
|
|
100
|
+
async checkpoint(args = {}) { return request("POST", "/api/agent/checkpoint", { body: args }); },
|
|
101
|
+
async complete(args = {}) { return request("POST", "/api/agent/complete", { body: args }); },
|
|
102
|
+
async archiveTicket(args = {}) { return request("POST", `/api/tickets/${encodeURIComponent(args.ticket_id)}/archive`, { query: ticketQuery(args) }); },
|
|
103
|
+
async restoreTicket(args = {}) { return request("POST", `/api/tickets/${encodeURIComponent(args.ticket_id)}/restore`, { query: ticketQuery(args) }); },
|
|
104
|
+
async deleteTicket(args = {}) { return request("DELETE", `/api/tickets/${encodeURIComponent(args.ticket_id)}`, { query: ticketQuery(args) }); },
|
|
105
|
+
async listArchive(args = {}) { return request("GET", `/api/boards/${encodeURIComponent(requireBoard(args))}/archive`); },
|
|
106
|
+
async exportBoard(args = {}) {
|
|
107
|
+
return request("GET", `/api/boards/${encodeURIComponent(requireBoard(args))}/export`, {
|
|
108
|
+
query: { include_attachments: args.include_attachments || args.include_images ? "true" : undefined }
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
async updateSettings(args = {}) { const { board_id, board_slug, board, ...patch } = args; return request("PATCH", `/api/boards/${encodeURIComponent(requireBoard(args))}`, { body: patch }); }
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function createLocalOrbitClient(env = process.env) {
|
|
116
|
+
const board = await import("../core/board.js");
|
|
117
|
+
const dbMod = await import("../core/db.js");
|
|
118
|
+
const registry = await import("../core/registry.js");
|
|
119
|
+
const backups = await import("../core/backups.js");
|
|
120
|
+
const seed = await import("../core/seed.js");
|
|
121
|
+
const util = await import("../core/util.js");
|
|
122
|
+
|
|
123
|
+
const provision = await import("../core/provision-repo-board.js");
|
|
124
|
+
let sessionBoardRow = null;
|
|
125
|
+
const setSessionBoard = (row) => { sessionBoardRow = row; if (row) registry.touchBoardActive(row.id); };
|
|
126
|
+
const getSessionBoard = () => sessionBoardRow;
|
|
127
|
+
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; } };
|
|
128
|
+
|
|
129
|
+
const start = util.normalizePath(env.PROJECT_ROOT ? resolve(env.PROJECT_ROOT) : process.cwd());
|
|
130
|
+
const displayStart = resolve(env.PROJECT_ROOT ? env.PROJECT_ROOT : process.cwd());
|
|
131
|
+
|
|
132
|
+
// Find repo root (walk up for .git, fall back to start).
|
|
133
|
+
const gitRoot = walkUp(start, (dir) => existsSync(join(dir, ".git")));
|
|
134
|
+
const repoRoot = util.normalizePath(gitRoot || start);
|
|
135
|
+
|
|
136
|
+
// 1. Registry-first lookup: walk upward from start so we find boards regardless
|
|
137
|
+
// of whether they were registered at the git root, at PROJECT_ROOT, or at any
|
|
138
|
+
// ancestor (handles boards originally created from a subdirectory).
|
|
139
|
+
let foundByRegistry = false;
|
|
140
|
+
{
|
|
141
|
+
let dir = start;
|
|
142
|
+
while (true) {
|
|
143
|
+
const row = registry.getBoardByRepoPath(dir);
|
|
144
|
+
if (row && existsSync(row.db_path)) { setSessionBoard(row); foundByRegistry = true; break; }
|
|
145
|
+
const parent = util.normalizePath(dirname(dir));
|
|
146
|
+
if (parent === dir) break; // filesystem root
|
|
147
|
+
dir = parent;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!foundByRegistry) {
|
|
151
|
+
// 2. Legacy fallback: walk up for an existing in-repo .orbit/board.db.
|
|
152
|
+
const boardRoot = walkUp(start, (dir) => existsSync(join(dir, ".orbit", "board.db")));
|
|
153
|
+
if (boardRoot) {
|
|
154
|
+
const root = util.normalizePath(boardRoot);
|
|
155
|
+
const existing = registry.getBoardByRepoPath(root);
|
|
156
|
+
if (existing) {
|
|
157
|
+
setSessionBoard(existing);
|
|
158
|
+
} else {
|
|
159
|
+
const dbPath = join(boardRoot, ".orbit", "board.db");
|
|
160
|
+
const db = dbMod.openConnection(dbPath);
|
|
161
|
+
dbMod.createBoardSchema(db);
|
|
162
|
+
const seeded = seed.seedIfEmpty(db, root);
|
|
163
|
+
if (seeded) {
|
|
164
|
+
const t = util.now();
|
|
165
|
+
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 });
|
|
166
|
+
}
|
|
167
|
+
const fresh = registry.getBoardByRepoPath(root);
|
|
168
|
+
if (fresh) { registry.syncRegistryFromBoardDb(fresh, db); setSessionBoard(fresh); }
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
// 3. No board found — auto-create one (goes to central DATA_DIR/boards/).
|
|
172
|
+
provision.provisionRepoBoard(repoRoot, { enableAi: true });
|
|
173
|
+
const fresh = registry.getBoardByRepoPath(repoRoot);
|
|
174
|
+
if (fresh) setSessionBoard(fresh);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const ctxFor = (row, actor) => {
|
|
179
|
+
if (!row) throw rpcError(-32602, "No board in session; pass board_id or board_slug, or call board_set_active first.");
|
|
180
|
+
return { actor, board: row, db: registry.openBoardDb(row) };
|
|
181
|
+
};
|
|
182
|
+
const sessionCtx = (actor) => ctxFor(getSessionBoard(), actor);
|
|
183
|
+
const backup = (ctx, result) => { backups.scheduleAutomaticBoardBackup(ctx.board, ctx.db); return result; };
|
|
184
|
+
const explicit = (args = {}) => args.board_id ? registry.getBoardByRegistryId(args.board_id) : (args.board_slug || args.board ? registry.getBoardBySlug(args.board_slug || args.board) : null);
|
|
185
|
+
const rowOrSession = (args = {}) => explicit(args) || getSessionBoard();
|
|
186
|
+
const actor = () => board.localAgentActor();
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
mode: "local",
|
|
190
|
+
sessionLabel: () => {
|
|
191
|
+
const b = getSessionBoard();
|
|
192
|
+
return b ? `${displayStart} target: ${b.repo_path} db: ${b.db_path}` : "(none)";
|
|
193
|
+
},
|
|
194
|
+
close: () => dbMod.closeAllConnections(),
|
|
195
|
+
boardContext: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getBoardContext(ctx.board.id, ctx, { includeStruck: Boolean(args.include_struck) }); },
|
|
196
|
+
boardList: () => ({ boards: registry.listBoards().map((row) => ({ id: row.id, slug: row.slug, name: row.name, repo_path: row.repo_path })) }),
|
|
197
|
+
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 }; },
|
|
198
|
+
claimNext: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.claimNext(args, ctx)); },
|
|
199
|
+
getTicketContext: (args = {}) => board.getContextPack(args.ticket_id, sessionCtx(actor()), Number(args.depth || 1)),
|
|
200
|
+
readTicket: (args = {}) => board.readTicket(args, sessionCtx(actor())),
|
|
201
|
+
readComments: (args = {}) => board.readComments(args, sessionCtx(actor())),
|
|
202
|
+
getTicketRelations: (args = {}) => board.getTicketRelations(args.ticket_id, sessionCtx(actor())),
|
|
203
|
+
getTicketBlockers: (args = {}) => board.getTicketBlockers(args.ticket_id, sessionCtx(actor())),
|
|
204
|
+
search: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.searchTickets({ q: args.q, limit: args.limit }, ctx); },
|
|
205
|
+
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)); },
|
|
206
|
+
updateTicket: ({ ticket_id, ...patch }) => { const ctx = sessionCtx(actor()); return backup(ctx, board.updateTicket(ticket_id, patch, ctx)); },
|
|
207
|
+
addComment: ({ ticket_id, ...body }) => { const ctx = sessionCtx(actor()); return backup(ctx, board.createComment(ticket_id, body, ctx)); },
|
|
208
|
+
addBoardEntry: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.createBoardEntry(ctx.board.id, args, ctx)); },
|
|
209
|
+
checkpoint: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.checkpointTicket(args, ctx)); },
|
|
210
|
+
complete: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.completeTicket(args, ctx)); },
|
|
211
|
+
archiveTicket: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.archiveTicket(args.ticket_id, ctx)); },
|
|
212
|
+
restoreTicket: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.restoreTicket(args.ticket_id, ctx)); },
|
|
213
|
+
deleteTicket: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.deleteTicket(args.ticket_id, ctx)); },
|
|
214
|
+
listArchive: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return { tickets: board.archivedTicketsForBoard(ctx.db, ctx.board.id) }; },
|
|
215
|
+
exportBoard: (args = {}) => {
|
|
216
|
+
const ctx = ctxFor(rowOrSession(args), actor());
|
|
217
|
+
return board.exportBoard(ctx.board.id, ctx, {
|
|
218
|
+
includeAttachments: Boolean(args.include_attachments || args.include_images)
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
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)); }
|
|
222
|
+
};
|
|
223
|
+
}
|
package/src/mcp-server.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
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 { createOrbitClient } from "./mcp/orbit-client.js";
|
|
7
8
|
import { existsSync, mkdirSync } from "node:fs";
|
|
8
9
|
import { dirname, join, resolve } from "node:path";
|
|
9
10
|
import {
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
completeTicket,
|
|
15
16
|
createBoardEntry,
|
|
16
17
|
createComment,
|
|
18
|
+
createTicket,
|
|
17
19
|
deleteTicket,
|
|
18
20
|
exportBoard,
|
|
19
21
|
getBoardContext,
|
|
@@ -28,7 +30,7 @@ import {
|
|
|
28
30
|
updateBoard,
|
|
29
31
|
updateTicket
|
|
30
32
|
} from "./core/board.js";
|
|
31
|
-
import { createBoardSchema, openConnection } from "./core/db.js";
|
|
33
|
+
import { closeAllConnections, createBoardSchema, openConnection } from "./core/db.js";
|
|
32
34
|
import {
|
|
33
35
|
getBoardByRegistryId,
|
|
34
36
|
getBoardBySlug,
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
touchBoardActive
|
|
41
43
|
} from "./core/registry.js";
|
|
42
44
|
import { scheduleAutomaticBoardBackup } from "./core/backups.js";
|
|
45
|
+
import { provisionRepoBoard } from "./core/provision-repo-board.js";
|
|
43
46
|
import { seedIfEmpty } from "./core/seed.js";
|
|
44
47
|
import { now, normalizePath } from "./core/util.js";
|
|
45
48
|
|
|
@@ -48,6 +51,7 @@ const SERVER_INFO = {
|
|
|
48
51
|
version: "0.1.0"
|
|
49
52
|
};
|
|
50
53
|
|
|
54
|
+
const orbitClient = await createOrbitClient();
|
|
51
55
|
// Per-process session board. Set on init by walking up from cwd, mutable via
|
|
52
56
|
// `board_set_active`. NEVER read by anything outside this file — every
|
|
53
57
|
// downstream call receives an explicit ctx.
|
|
@@ -88,7 +92,16 @@ function walkUp(startDir, predicate) {
|
|
|
88
92
|
function initMcpSessionBoard() {
|
|
89
93
|
const start = normalizePath(process.env.PROJECT_ROOT ? resolve(process.env.PROJECT_ROOT) : process.cwd());
|
|
90
94
|
|
|
91
|
-
// 1.
|
|
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.
|
|
92
105
|
const boardRoot = walkUp(start, (dir) => existsSync(join(dir, ".orbit", "board.db")));
|
|
93
106
|
if (boardRoot) {
|
|
94
107
|
const root = normalizePath(boardRoot);
|
|
@@ -108,9 +121,10 @@ function initMcpSessionBoard() {
|
|
|
108
121
|
return;
|
|
109
122
|
}
|
|
110
123
|
|
|
111
|
-
//
|
|
124
|
+
// 3. Walk up for a git root; fall back to cwd for non-git projects.
|
|
112
125
|
const gitRoot = walkUp(start, (dir) => existsSync(join(dir, ".git")));
|
|
113
|
-
|
|
126
|
+
const result = provisionRepoBoard(normalizePath(gitRoot || start));
|
|
127
|
+
if (result.registryRow) setSessionBoard(result.registryRow);
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
function createBoardAtRoot(root) {
|
|
@@ -128,6 +142,7 @@ function createBoardAtRoot(root) {
|
|
|
128
142
|
}
|
|
129
143
|
|
|
130
144
|
initMcpSessionBoard();
|
|
145
|
+
closeAllConnections();
|
|
131
146
|
|
|
132
147
|
/** Build a ctx for the session board (or a board the tool argument selected). */
|
|
133
148
|
function ctxFor(boardRow, actor) {
|
|
@@ -180,12 +195,7 @@ const TOOL_DEFS = [
|
|
|
180
195
|
},
|
|
181
196
|
additionalProperties: false
|
|
182
197
|
},
|
|
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
|
-
}
|
|
198
|
+
handler: (args) => orbitClient.boardContext(args)
|
|
189
199
|
},
|
|
190
200
|
{
|
|
191
201
|
name: "board_list",
|
|
@@ -196,16 +206,7 @@ const TOOL_DEFS = [
|
|
|
196
206
|
properties: {},
|
|
197
207
|
additionalProperties: false
|
|
198
208
|
},
|
|
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
|
-
}
|
|
209
|
+
handler: () => orbitClient.boardList()
|
|
209
210
|
},
|
|
210
211
|
{
|
|
211
212
|
name: "board_set_active",
|
|
@@ -219,14 +220,7 @@ const TOOL_DEFS = [
|
|
|
219
220
|
required: ["slug"],
|
|
220
221
|
additionalProperties: false
|
|
221
222
|
},
|
|
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
|
-
}
|
|
223
|
+
handler: (args) => orbitClient.boardSetActive(args)
|
|
230
224
|
},
|
|
231
225
|
{
|
|
232
226
|
name: "board_claim_next",
|
|
@@ -243,17 +237,12 @@ const TOOL_DEFS = [
|
|
|
243
237
|
},
|
|
244
238
|
additionalProperties: false
|
|
245
239
|
},
|
|
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
|
-
}
|
|
240
|
+
handler: (args) => orbitClient.claimNext(args)
|
|
252
241
|
},
|
|
253
242
|
{
|
|
254
243
|
name: "board_get_ticket_context",
|
|
255
244
|
description:
|
|
256
|
-
"Read the
|
|
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.",
|
|
257
246
|
inputSchema: {
|
|
258
247
|
type: "object",
|
|
259
248
|
properties: {
|
|
@@ -263,16 +252,12 @@ const TOOL_DEFS = [
|
|
|
263
252
|
required: ["ticket_id"],
|
|
264
253
|
additionalProperties: false
|
|
265
254
|
},
|
|
266
|
-
handler: (args) =>
|
|
267
|
-
const actor = localAgentActor();
|
|
268
|
-
const ctx = sessionCtx(actor);
|
|
269
|
-
return getContextPack(args.ticket_id, ctx, Number(args.depth || 1));
|
|
270
|
-
}
|
|
255
|
+
handler: (args) => orbitClient.getTicketContext(args)
|
|
271
256
|
},
|
|
272
257
|
{
|
|
273
258
|
name: "board_read_ticket",
|
|
274
259
|
description:
|
|
275
|
-
"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
|
|
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.",
|
|
276
261
|
inputSchema: {
|
|
277
262
|
type: "object",
|
|
278
263
|
properties: {
|
|
@@ -282,11 +267,7 @@ const TOOL_DEFS = [
|
|
|
282
267
|
},
|
|
283
268
|
additionalProperties: false
|
|
284
269
|
},
|
|
285
|
-
handler: (args) =>
|
|
286
|
-
const actor = localAgentActor();
|
|
287
|
-
const ctx = sessionCtx(actor);
|
|
288
|
-
return readTicket(args, ctx);
|
|
289
|
-
}
|
|
270
|
+
handler: (args) => orbitClient.readTicket(args)
|
|
290
271
|
},
|
|
291
272
|
{
|
|
292
273
|
name: "board_read_comments",
|
|
@@ -301,11 +282,7 @@ const TOOL_DEFS = [
|
|
|
301
282
|
},
|
|
302
283
|
additionalProperties: false
|
|
303
284
|
},
|
|
304
|
-
handler: (args) =>
|
|
305
|
-
const actor = localAgentActor();
|
|
306
|
-
const ctx = sessionCtx(actor);
|
|
307
|
-
return readComments(args, ctx);
|
|
308
|
-
}
|
|
285
|
+
handler: (args) => orbitClient.readComments(args)
|
|
309
286
|
},
|
|
310
287
|
{
|
|
311
288
|
name: "board_get_ticket_relations",
|
|
@@ -318,10 +295,7 @@ const TOOL_DEFS = [
|
|
|
318
295
|
required: ["ticket_id"],
|
|
319
296
|
additionalProperties: false
|
|
320
297
|
},
|
|
321
|
-
handler: (args) =>
|
|
322
|
-
const actor = localAgentActor();
|
|
323
|
-
return getTicketRelations(args.ticket_id, sessionCtx(actor));
|
|
324
|
-
}
|
|
298
|
+
handler: (args) => orbitClient.getTicketRelations(args)
|
|
325
299
|
},
|
|
326
300
|
{
|
|
327
301
|
name: "board_get_ticket_blockers",
|
|
@@ -334,10 +308,7 @@ const TOOL_DEFS = [
|
|
|
334
308
|
required: ["ticket_id"],
|
|
335
309
|
additionalProperties: false
|
|
336
310
|
},
|
|
337
|
-
handler: (args) =>
|
|
338
|
-
const actor = localAgentActor();
|
|
339
|
-
return getTicketBlockers(args.ticket_id, sessionCtx(actor));
|
|
340
|
-
}
|
|
311
|
+
handler: (args) => orbitClient.getTicketBlockers(args)
|
|
341
312
|
},
|
|
342
313
|
{
|
|
343
314
|
name: "board_search",
|
|
@@ -355,12 +326,33 @@ const TOOL_DEFS = [
|
|
|
355
326
|
required: ["q"],
|
|
356
327
|
additionalProperties: false
|
|
357
328
|
},
|
|
358
|
-
handler: (args) =>
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
329
|
+
handler: (args) => orbitClient.search(args)
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: "board_create_ticket",
|
|
333
|
+
description:
|
|
334
|
+
"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.",
|
|
335
|
+
inputSchema: {
|
|
336
|
+
type: "object",
|
|
337
|
+
properties: {
|
|
338
|
+
board_id: { type: "string" },
|
|
339
|
+
board_slug: { type: "string" },
|
|
340
|
+
board: { type: "string" },
|
|
341
|
+
title: { type: "string" },
|
|
342
|
+
description: { type: "string" },
|
|
343
|
+
type: { type: "string", enum: ["epic", "feature", "task", "bug"] },
|
|
344
|
+
parent_ticket_id: { type: ["string", "null"] },
|
|
345
|
+
ai_plan: { type: "string" },
|
|
346
|
+
implementation_summary: { type: "string" },
|
|
347
|
+
implementation_updates: { type: "string" },
|
|
348
|
+
state_id: { type: "string" },
|
|
349
|
+
priority: { type: "integer" },
|
|
350
|
+
labels: { type: "array", items: { type: "string" } }
|
|
351
|
+
},
|
|
352
|
+
required: ["title"],
|
|
353
|
+
additionalProperties: false
|
|
354
|
+
},
|
|
355
|
+
handler: (args) => orbitClient.createTicket(args)
|
|
364
356
|
},
|
|
365
357
|
{
|
|
366
358
|
name: "board_update_ticket",
|
|
@@ -384,11 +376,7 @@ const TOOL_DEFS = [
|
|
|
384
376
|
required: ["ticket_id"],
|
|
385
377
|
additionalProperties: false
|
|
386
378
|
},
|
|
387
|
-
handler: ({ ticket_id, ...patch }) => {
|
|
388
|
-
const actor = localAgentActor();
|
|
389
|
-
const ctx = sessionCtx(actor);
|
|
390
|
-
return withAutomaticBackup(ctx, updateTicket(ticket_id, patch, ctx));
|
|
391
|
-
}
|
|
379
|
+
handler: ({ ticket_id, ...patch }) => orbitClient.updateTicket({ ticket_id, ...patch })
|
|
392
380
|
},
|
|
393
381
|
{
|
|
394
382
|
name: "board_add_comment",
|
|
@@ -403,11 +391,7 @@ const TOOL_DEFS = [
|
|
|
403
391
|
required: ["ticket_id", "body"],
|
|
404
392
|
additionalProperties: false
|
|
405
393
|
},
|
|
406
|
-
handler: ({ ticket_id, ...body }) => {
|
|
407
|
-
const actor = localAgentActor();
|
|
408
|
-
const ctx = sessionCtx(actor);
|
|
409
|
-
return withAutomaticBackup(ctx, createComment(ticket_id, body, ctx));
|
|
410
|
-
}
|
|
394
|
+
handler: ({ ticket_id, ...body }) => orbitClient.addComment({ ticket_id, ...body })
|
|
411
395
|
},
|
|
412
396
|
{
|
|
413
397
|
name: "board_add_board_entry",
|
|
@@ -425,12 +409,7 @@ const TOOL_DEFS = [
|
|
|
425
409
|
required: ["type", "title"],
|
|
426
410
|
additionalProperties: false
|
|
427
411
|
},
|
|
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
|
-
}
|
|
412
|
+
handler: (args) => orbitClient.addBoardEntry(args)
|
|
434
413
|
},
|
|
435
414
|
{
|
|
436
415
|
name: "board_checkpoint",
|
|
@@ -445,11 +424,7 @@ const TOOL_DEFS = [
|
|
|
445
424
|
required: ["ticket_id", "message"],
|
|
446
425
|
additionalProperties: false
|
|
447
426
|
},
|
|
448
|
-
handler: (args) =>
|
|
449
|
-
const actor = localAgentActor();
|
|
450
|
-
const ctx = sessionCtx(actor);
|
|
451
|
-
return withAutomaticBackup(ctx, checkpointTicket(args, ctx));
|
|
452
|
-
}
|
|
427
|
+
handler: (args) => orbitClient.checkpoint(args)
|
|
453
428
|
},
|
|
454
429
|
{
|
|
455
430
|
name: "board_complete",
|
|
@@ -467,11 +442,7 @@ const TOOL_DEFS = [
|
|
|
467
442
|
required: ["ticket_id", "summary"],
|
|
468
443
|
additionalProperties: false
|
|
469
444
|
},
|
|
470
|
-
handler: (args) =>
|
|
471
|
-
const actor = localAgentActor();
|
|
472
|
-
const ctx = sessionCtx(actor);
|
|
473
|
-
return withAutomaticBackup(ctx, completeTicket(args, ctx));
|
|
474
|
-
}
|
|
445
|
+
handler: (args) => orbitClient.complete(args)
|
|
475
446
|
},
|
|
476
447
|
{
|
|
477
448
|
name: "board_archive_ticket",
|
|
@@ -483,11 +454,7 @@ const TOOL_DEFS = [
|
|
|
483
454
|
required: ["ticket_id"],
|
|
484
455
|
additionalProperties: false
|
|
485
456
|
},
|
|
486
|
-
handler: (args) =>
|
|
487
|
-
const actor = localAgentActor();
|
|
488
|
-
const ctx = sessionCtx(actor);
|
|
489
|
-
return withAutomaticBackup(ctx, archiveTicket(args.ticket_id, ctx));
|
|
490
|
-
}
|
|
457
|
+
handler: (args) => orbitClient.archiveTicket(args)
|
|
491
458
|
},
|
|
492
459
|
{
|
|
493
460
|
name: "board_restore_ticket",
|
|
@@ -498,11 +465,7 @@ const TOOL_DEFS = [
|
|
|
498
465
|
required: ["ticket_id"],
|
|
499
466
|
additionalProperties: false
|
|
500
467
|
},
|
|
501
|
-
handler: (args) =>
|
|
502
|
-
const actor = localAgentActor();
|
|
503
|
-
const ctx = sessionCtx(actor);
|
|
504
|
-
return withAutomaticBackup(ctx, restoreTicket(args.ticket_id, ctx));
|
|
505
|
-
}
|
|
468
|
+
handler: (args) => orbitClient.restoreTicket(args)
|
|
506
469
|
},
|
|
507
470
|
{
|
|
508
471
|
name: "board_delete_ticket",
|
|
@@ -514,11 +477,7 @@ const TOOL_DEFS = [
|
|
|
514
477
|
required: ["ticket_id"],
|
|
515
478
|
additionalProperties: false
|
|
516
479
|
},
|
|
517
|
-
handler: (args) =>
|
|
518
|
-
const actor = localAgentActor();
|
|
519
|
-
const ctx = sessionCtx(actor);
|
|
520
|
-
return withAutomaticBackup(ctx, deleteTicket(args.ticket_id, ctx));
|
|
521
|
-
}
|
|
480
|
+
handler: (args) => orbitClient.deleteTicket(args)
|
|
522
481
|
},
|
|
523
482
|
{
|
|
524
483
|
name: "board_list_archive",
|
|
@@ -531,12 +490,7 @@ const TOOL_DEFS = [
|
|
|
531
490
|
},
|
|
532
491
|
additionalProperties: false
|
|
533
492
|
},
|
|
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
|
-
}
|
|
493
|
+
handler: (args) => orbitClient.listArchive(args)
|
|
540
494
|
},
|
|
541
495
|
{
|
|
542
496
|
name: "board_export_board",
|
|
@@ -545,21 +499,18 @@ const TOOL_DEFS = [
|
|
|
545
499
|
type: "object",
|
|
546
500
|
properties: {
|
|
547
501
|
board_id: { type: "string" },
|
|
548
|
-
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." }
|
|
549
505
|
},
|
|
550
506
|
additionalProperties: false
|
|
551
507
|
},
|
|
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
|
-
}
|
|
508
|
+
handler: (args) => orbitClient.exportBoard(args)
|
|
558
509
|
},
|
|
559
510
|
{
|
|
560
511
|
name: "board_update_settings",
|
|
561
512
|
description:
|
|
562
|
-
"PATCH-equivalent for board settings: name, repo_url, system_path, default_branch, agent_instructions (project-level agent context), and project_notes (Notes For You).
|
|
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).",
|
|
563
514
|
inputSchema: {
|
|
564
515
|
type: "object",
|
|
565
516
|
properties: {
|
|
@@ -574,13 +525,7 @@ const TOOL_DEFS = [
|
|
|
574
525
|
},
|
|
575
526
|
additionalProperties: false
|
|
576
527
|
},
|
|
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
|
-
}
|
|
528
|
+
handler: (args) => orbitClient.updateSettings(args)
|
|
584
529
|
}
|
|
585
530
|
];
|
|
586
531
|
|
|
@@ -588,13 +533,21 @@ const tools = new Map(TOOL_DEFS.map((tool) => [tool.name, tool]));
|
|
|
588
533
|
|
|
589
534
|
let inputBuffer = Buffer.alloc(0);
|
|
590
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
|
+
}
|
|
591
544
|
|
|
592
545
|
process.stdin.on("data", (chunk) => {
|
|
593
546
|
inputBuffer = Buffer.concat([inputBuffer, chunk]);
|
|
594
547
|
drainInput();
|
|
595
548
|
});
|
|
596
549
|
|
|
597
|
-
process.stdin.on("end", () =>
|
|
550
|
+
process.stdin.on("end", () => shutdown(0));
|
|
598
551
|
process.stdin.resume();
|
|
599
552
|
|
|
600
553
|
function drainInput() {
|
|
@@ -673,6 +626,8 @@ async function handleMessage(body, transport = "content-length") {
|
|
|
673
626
|
}, transport);
|
|
674
627
|
} catch (error) {
|
|
675
628
|
writeMessage(errorResponse(message.id, error.code || -32000, error.message || "Internal error."), transport);
|
|
629
|
+
} finally {
|
|
630
|
+
orbitClient.close?.();
|
|
676
631
|
}
|
|
677
632
|
}
|
|
678
633
|
|
|
@@ -768,6 +723,11 @@ process.on("unhandledRejection", (error) => {
|
|
|
768
723
|
writeMessage(errorResponse(null, -32000, error?.message || "Unhandled rejection."));
|
|
769
724
|
});
|
|
770
725
|
|
|
726
|
+
process.once("SIGINT", () => shutdown(0));
|
|
727
|
+
process.once("SIGTERM", () => shutdown(0));
|
|
728
|
+
process.once("exit", () => orbitClient.close?.());
|
|
729
|
+
|
|
771
730
|
if (process.env.MAB_MCP_STDERR_LOG === "1") {
|
|
772
|
-
|
|
731
|
+
const target = typeof orbitClient.sessionLabel === "function" ? orbitClient.sessionLabel() : orbitClient.sessionLabel;
|
|
732
|
+
console.error(`Starscape Orbit MCP ready. Mode: ${orbitClient.mode}. Target: ${target}`);
|
|
773
733
|
}
|