@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.
- package/SKILL-ORBIT.md +2 -0
- package/build.mjs +2 -2
- package/dist/full/index.html +2 -2
- package/dist/full/js/app.f4fd5e94.js +604 -0
- package/dist/full/js/app.f4fd5e94.js.map +7 -0
- package/dist/full/{styles.c2e3d20a.css → styles.14e9dbbd.css} +295 -22
- package/dist/full/version.json +2 -2
- package/docs/AGENT_PROTOCOL.md +4 -0
- package/docs/MCP_SETUP.md +3 -1
- package/package.json +2 -2
- package/public/js/api.js +5 -1
- package/public/js/drawer.js +2 -0
- package/public/js/format.js +142 -0
- package/public/js/kanban.js +10 -2
- package/public/js/main.js +14 -6
- package/public/js/motion-preference.js +53 -0
- package/public/js/router.js +75 -56
- package/public/js/settings-tabs.js +1 -1
- package/public/js/settings.js +96 -3
- package/public/js/starscape.js +25 -18
- package/public/js/state.js +11 -2
- package/public/js/theme-preference.js +88 -0
- package/public/js/ticket-detail.js +6 -3
- package/public/js/url-routes.js +94 -0
- package/public/styles.css +295 -22
- package/src/cli/orbit.js +0 -0
- package/src/core/agents-md.js +1 -0
- package/src/core/board.js +1 -1
- package/src/core/bootstrap.js +9 -1
- package/src/core/router.js +1 -1
- package/src/core/search.js +56 -22
- package/src/mcp/orbit-client.js +180 -0
- package/src/mcp-server.js +50 -274
- package/dist/full/js/app.aedc1bfe.js +0 -554
- package/dist/full/js/app.aedc1bfe.js.map +0 -7
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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.
|
|
548
|
+
console.error(`Starscape Orbit MCP ready. Mode: ${orbitClient.mode}. Target: ${typeof orbitClient.sessionLabel === "function" ? orbitClient.sessionLabel() : orbitClient.sessionLabel}`);
|
|
773
549
|
}
|