@starlove/orbit 1.0.7 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -7
- package/SKILL-ORBIT.md +26 -1
- 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.14e9dbbd.css → styles.836872e7.css} +836 -39
- package/dist/full/version.json +2 -2
- package/docs/AGENT_PROTOCOL.md +23 -5
- package/docs/MCP_SETUP.md +5 -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/app.js +8 -1
- package/public/js/card-actions.js +526 -0
- package/public/js/config.js +2 -1
- package/public/js/format.js +204 -10
- package/public/js/kanban.js +15 -0
- package/public/js/local-backend.js +24 -8
- package/public/js/router.js +3 -2
- package/public/js/search.js +6 -3
- package/public/js/settings.js +96 -26
- package/public/js/state.js +50 -0
- package/public/js/ticket-detail.js +428 -125
- package/public/styles.css +836 -39
- package/src/cli/orbit.js +217 -32
- package/src/core/agent.js +114 -24
- package/src/core/agents-md.js +15 -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/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 +81 -7
- package/src/core/search.js +83 -10
- package/src/core/snapshots.js +14 -2
- package/src/mcp/orbit-client.js +109 -46
- package/src/mcp-server.js +238 -12
- package/src/server.js +20 -0
- package/dist/full/js/app.f4fd5e94.js +0 -604
- package/dist/full/js/app.f4fd5e94.js.map +0 -7
package/src/mcp/orbit-client.js
CHANGED
|
@@ -57,26 +57,61 @@ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const ticketQuery = (args = {}) => ({ board_id: args.board_id, board: args.board_slug || args.board || defaultBoard });
|
|
60
|
+
const searchQuery = (args = {}) => ({
|
|
61
|
+
q: args.q,
|
|
62
|
+
limit: args.limit,
|
|
63
|
+
board_id: args.board_id,
|
|
64
|
+
board: args.board_slug || args.board || defaultBoard,
|
|
65
|
+
mode: args.mode,
|
|
66
|
+
include_full: args.include_full || args.includeFull ? "true" : undefined,
|
|
67
|
+
fields: Array.isArray(args.fields) ? args.fields.join(",") : args.fields,
|
|
68
|
+
max_chars_per_field: args.max_chars_per_field ?? args.maxCharsPerField
|
|
69
|
+
});
|
|
70
|
+
const contextQuery = (args = {}) => ({
|
|
71
|
+
...ticketQuery(args),
|
|
72
|
+
depth: args.depth,
|
|
73
|
+
max_chars_per_field: args.max_chars_per_field ?? args.maxCharsPerField,
|
|
74
|
+
comment_limit: args.comment_limit ?? args.commentLimit,
|
|
75
|
+
include_parent_full: args.include_parent_full || args.includeParentFull ? "true" : undefined,
|
|
76
|
+
include_related_full: args.include_related_full || args.includeRelatedFull ? "true" : undefined
|
|
77
|
+
});
|
|
60
78
|
|
|
61
79
|
return {
|
|
62
80
|
mode: "remote",
|
|
63
81
|
sessionLabel: apiUrl,
|
|
82
|
+
close() {},
|
|
64
83
|
async boardContext(args = {}) {
|
|
65
84
|
return request("GET", `/api/boards/${encodeURIComponent(requireBoard(args))}/context`, { query: { include_struck: args.include_struck ? "true" : undefined } });
|
|
66
85
|
},
|
|
67
86
|
async boardList() { return request("GET", "/api/boards"); },
|
|
68
87
|
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
88
|
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: { ...
|
|
89
|
+
async getTicketContext(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/context`, { query: { ...contextQuery(args), depth: args.depth || 1 } }); },
|
|
90
|
+
async getAgentDispatchPacket(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/dispatch-packet`, { query: contextQuery(args) }); },
|
|
71
91
|
async readTicket(args = {}) {
|
|
72
|
-
if (args.ticket_id)
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
if (args.ticket_id) {
|
|
93
|
+
return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}`, { query: ticketQuery(args) });
|
|
94
|
+
}
|
|
95
|
+
if (args.number !== undefined || args.title) {
|
|
96
|
+
return request("GET", "/api/tickets/lookup", { query: { ...ticketQuery(args), number: args.number, title: args.title } });
|
|
97
|
+
}
|
|
98
|
+
throw rpcError(-32602, "ticket_id_or_number_or_title_required");
|
|
99
|
+
},
|
|
100
|
+
async readComments(args = {}) {
|
|
101
|
+
if (args.ticket_id) {
|
|
102
|
+
return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/comments`, { query: ticketQuery(args) });
|
|
103
|
+
}
|
|
104
|
+
const found = await this.readTicket(args);
|
|
105
|
+
const ticket = found.ticket || found.results?.find((row) =>
|
|
106
|
+
(args.number !== undefined && Number(row.number) === Number(args.number)) ||
|
|
107
|
+
(args.title && String(row.title || "").toLowerCase() === String(args.title).toLowerCase())
|
|
108
|
+
);
|
|
109
|
+
if (!ticket?.id) throw rpcError(-32004, "ticket_not_found");
|
|
110
|
+
return request("GET", `/api/tickets/${encodeURIComponent(ticket.id)}/comments`, { query: ticketQuery(args) });
|
|
75
111
|
},
|
|
76
|
-
async readComments(args = {}) { const ctx = await this.readTicket(args); return { comments: ctx.comments || ctx.ticket?.comments || [] }; },
|
|
77
112
|
async getTicketRelations(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/relations`, { query: ticketQuery(args) }); },
|
|
78
113
|
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:
|
|
114
|
+
async search(args = {}) { return request("GET", "/api/search", { query: searchQuery(args) }); },
|
|
80
115
|
async createTicket(args = {}) { return request("POST", "/api/tickets", { body: { ...args, board: requireBoard(args) }, ok: [201] }); },
|
|
81
116
|
async updateTicket({ ticket_id, ...patch }) { return request("PATCH", `/api/tickets/${encodeURIComponent(ticket_id)}`, { query: ticketQuery(patch), body: patch }); },
|
|
82
117
|
async addComment({ ticket_id, ...body }) { return request("POST", `/api/tickets/${encodeURIComponent(ticket_id)}/comments`, { body, ok: [201] }); },
|
|
@@ -87,7 +122,11 @@ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.
|
|
|
87
122
|
async restoreTicket(args = {}) { return request("POST", `/api/tickets/${encodeURIComponent(args.ticket_id)}/restore`, { query: ticketQuery(args) }); },
|
|
88
123
|
async deleteTicket(args = {}) { return request("DELETE", `/api/tickets/${encodeURIComponent(args.ticket_id)}`, { query: ticketQuery(args) }); },
|
|
89
124
|
async listArchive(args = {}) { return request("GET", `/api/boards/${encodeURIComponent(requireBoard(args))}/archive`); },
|
|
90
|
-
async exportBoard(args = {}) {
|
|
125
|
+
async exportBoard(args = {}) {
|
|
126
|
+
return request("GET", `/api/boards/${encodeURIComponent(requireBoard(args))}/export`, {
|
|
127
|
+
query: { include_attachments: args.include_attachments || args.include_images ? "true" : undefined }
|
|
128
|
+
});
|
|
129
|
+
},
|
|
91
130
|
async updateSettings(args = {}) { const { board_id, board_slug, board, ...patch } = args; return request("PATCH", `/api/boards/${encodeURIComponent(requireBoard(args))}`, { body: patch }); }
|
|
92
131
|
};
|
|
93
132
|
}
|
|
@@ -100,45 +139,59 @@ export async function createLocalOrbitClient(env = process.env) {
|
|
|
100
139
|
const seed = await import("../core/seed.js");
|
|
101
140
|
const util = await import("../core/util.js");
|
|
102
141
|
|
|
142
|
+
const provision = await import("../core/provision-repo-board.js");
|
|
103
143
|
let sessionBoardRow = null;
|
|
104
144
|
const setSessionBoard = (row) => { sessionBoardRow = row; if (row) registry.touchBoardActive(row.id); };
|
|
105
145
|
const getSessionBoard = () => sessionBoardRow;
|
|
106
146
|
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
147
|
|
|
121
148
|
const start = util.normalizePath(env.PROJECT_ROOT ? resolve(env.PROJECT_ROOT) : process.cwd());
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
149
|
+
const displayStart = resolve(env.PROJECT_ROOT ? env.PROJECT_ROOT : process.cwd());
|
|
150
|
+
|
|
151
|
+
// Find repo root (walk up for .git, fall back to start).
|
|
152
|
+
const gitRoot = walkUp(start, (dir) => existsSync(join(dir, ".git")));
|
|
153
|
+
const repoRoot = util.normalizePath(gitRoot || start);
|
|
154
|
+
|
|
155
|
+
// 1. Registry-first lookup: walk upward from start so we find boards regardless
|
|
156
|
+
// of whether they were registered at the git root, at PROJECT_ROOT, or at any
|
|
157
|
+
// ancestor (handles boards originally created from a subdirectory).
|
|
158
|
+
let foundByRegistry = false;
|
|
159
|
+
{
|
|
160
|
+
let dir = start;
|
|
161
|
+
while (true) {
|
|
162
|
+
const row = registry.getBoardByRepoPath(dir);
|
|
163
|
+
if (row && existsSync(row.db_path)) { setSessionBoard(row); foundByRegistry = true; break; }
|
|
164
|
+
const parent = util.normalizePath(dirname(dir));
|
|
165
|
+
if (parent === dir) break; // filesystem root
|
|
166
|
+
dir = parent;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!foundByRegistry) {
|
|
170
|
+
// 2. Legacy fallback: walk up for an existing in-repo .orbit/board.db.
|
|
171
|
+
const boardRoot = walkUp(start, (dir) => existsSync(join(dir, ".orbit", "board.db")));
|
|
172
|
+
if (boardRoot) {
|
|
173
|
+
const root = util.normalizePath(boardRoot);
|
|
174
|
+
const existing = registry.getBoardByRepoPath(root);
|
|
175
|
+
if (existing) {
|
|
176
|
+
setSessionBoard(existing);
|
|
177
|
+
} else {
|
|
178
|
+
const dbPath = join(boardRoot, ".orbit", "board.db");
|
|
179
|
+
const db = dbMod.openConnection(dbPath);
|
|
180
|
+
dbMod.createBoardSchema(db);
|
|
181
|
+
const seeded = seed.seedIfEmpty(db, root);
|
|
182
|
+
if (seeded) {
|
|
183
|
+
const t = util.now();
|
|
184
|
+
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 });
|
|
185
|
+
}
|
|
186
|
+
const fresh = registry.getBoardByRepoPath(root);
|
|
187
|
+
if (fresh) { registry.syncRegistryFromBoardDb(fresh, db); setSessionBoard(fresh); }
|
|
135
188
|
}
|
|
136
|
-
|
|
137
|
-
|
|
189
|
+
} else {
|
|
190
|
+
// 3. No board found — auto-create one (goes to central DATA_DIR/boards/).
|
|
191
|
+
provision.provisionRepoBoard(repoRoot, { enableAi: true });
|
|
192
|
+
const fresh = registry.getBoardByRepoPath(repoRoot);
|
|
193
|
+
if (fresh) setSessionBoard(fresh);
|
|
138
194
|
}
|
|
139
|
-
} else {
|
|
140
|
-
const gitRoot = walkUp(start, (dir) => existsSync(join(dir, ".git")));
|
|
141
|
-
createBoardAtRoot(util.normalizePath(gitRoot || start));
|
|
142
195
|
}
|
|
143
196
|
|
|
144
197
|
const ctxFor = (row, actor) => {
|
|
@@ -153,17 +206,22 @@ export async function createLocalOrbitClient(env = process.env) {
|
|
|
153
206
|
|
|
154
207
|
return {
|
|
155
208
|
mode: "local",
|
|
156
|
-
sessionLabel: () =>
|
|
209
|
+
sessionLabel: () => {
|
|
210
|
+
const b = getSessionBoard();
|
|
211
|
+
return b ? `${displayStart} target: ${b.repo_path} db: ${b.db_path}` : "(none)";
|
|
212
|
+
},
|
|
213
|
+
close: () => dbMod.closeAllConnections(),
|
|
157
214
|
boardContext: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getBoardContext(ctx.board.id, ctx, { includeStruck: Boolean(args.include_struck) }); },
|
|
158
215
|
boardList: () => ({ boards: registry.listBoards().map((row) => ({ id: row.id, slug: row.slug, name: row.name, repo_path: row.repo_path })) }),
|
|
159
216
|
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
217
|
claimNext: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.claimNext(args, ctx)); },
|
|
161
|
-
getTicketContext: (args = {}) => board.getContextPack(args.ticket_id,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
218
|
+
getTicketContext: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getContextPack(args.ticket_id, ctx, Number(args.depth || 1), args); },
|
|
219
|
+
getAgentDispatchPacket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getAgentDispatchPacket(args.ticket_id, ctx, args); },
|
|
220
|
+
readTicket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.readTicket(args, ctx); },
|
|
221
|
+
readComments: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.readComments(args, ctx); },
|
|
222
|
+
getTicketRelations: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getTicketRelations(args.ticket_id, ctx); },
|
|
223
|
+
getTicketBlockers: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getTicketBlockers(args.ticket_id, ctx); },
|
|
224
|
+
search: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.searchTickets(args, ctx); },
|
|
167
225
|
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
226
|
updateTicket: ({ ticket_id, ...patch }) => { const ctx = sessionCtx(actor()); return backup(ctx, board.updateTicket(ticket_id, patch, ctx)); },
|
|
169
227
|
addComment: ({ ticket_id, ...body }) => { const ctx = sessionCtx(actor()); return backup(ctx, board.createComment(ticket_id, body, ctx)); },
|
|
@@ -174,7 +232,12 @@ export async function createLocalOrbitClient(env = process.env) {
|
|
|
174
232
|
restoreTicket: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.restoreTicket(args.ticket_id, ctx)); },
|
|
175
233
|
deleteTicket: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.deleteTicket(args.ticket_id, ctx)); },
|
|
176
234
|
listArchive: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return { tickets: board.archivedTicketsForBoard(ctx.db, ctx.board.id) }; },
|
|
177
|
-
exportBoard: (args = {}) => {
|
|
235
|
+
exportBoard: (args = {}) => {
|
|
236
|
+
const ctx = ctxFor(rowOrSession(args), actor());
|
|
237
|
+
return board.exportBoard(ctx.board.id, ctx, {
|
|
238
|
+
includeAttachments: Boolean(args.include_attachments || args.include_images)
|
|
239
|
+
});
|
|
240
|
+
},
|
|
178
241
|
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
242
|
};
|
|
180
243
|
}
|
package/src/mcp-server.js
CHANGED
|
@@ -5,6 +5,47 @@
|
|
|
5
5
|
// "active board" state.
|
|
6
6
|
|
|
7
7
|
import { createOrbitClient } from "./mcp/orbit-client.js";
|
|
8
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
archivedTicketsForBoard,
|
|
12
|
+
archiveTicket,
|
|
13
|
+
checkpointTicket,
|
|
14
|
+
claimNext,
|
|
15
|
+
completeTicket,
|
|
16
|
+
createBoardEntry,
|
|
17
|
+
createComment,
|
|
18
|
+
createTicket,
|
|
19
|
+
deleteTicket,
|
|
20
|
+
exportBoard,
|
|
21
|
+
getBoardContext,
|
|
22
|
+
getAgentDispatchPacket,
|
|
23
|
+
getContextPack,
|
|
24
|
+
readComments,
|
|
25
|
+
readTicket,
|
|
26
|
+
getTicketBlockers,
|
|
27
|
+
getTicketRelations,
|
|
28
|
+
localAgentActor,
|
|
29
|
+
restoreTicket,
|
|
30
|
+
searchTickets,
|
|
31
|
+
updateBoard,
|
|
32
|
+
updateTicket
|
|
33
|
+
} from "./core/board.js";
|
|
34
|
+
import { closeAllConnections, createBoardSchema, openConnection } from "./core/db.js";
|
|
35
|
+
import {
|
|
36
|
+
getBoardByRegistryId,
|
|
37
|
+
getBoardBySlug,
|
|
38
|
+
getBoardByRepoPath,
|
|
39
|
+
insertBoard,
|
|
40
|
+
listBoards,
|
|
41
|
+
openBoardDb,
|
|
42
|
+
syncRegistryFromBoardDb,
|
|
43
|
+
touchBoardActive
|
|
44
|
+
} from "./core/registry.js";
|
|
45
|
+
import { scheduleAutomaticBoardBackup } from "./core/backups.js";
|
|
46
|
+
import { provisionRepoBoard } from "./core/provision-repo-board.js";
|
|
47
|
+
import { seedIfEmpty } from "./core/seed.js";
|
|
48
|
+
import { now, normalizePath } from "./core/util.js";
|
|
8
49
|
|
|
9
50
|
const SERVER_INFO = {
|
|
10
51
|
name: "minimal-agent-board",
|
|
@@ -12,6 +53,133 @@ const SERVER_INFO = {
|
|
|
12
53
|
};
|
|
13
54
|
|
|
14
55
|
const orbitClient = await createOrbitClient();
|
|
56
|
+
// Per-process session board. Set on init by walking up from cwd, mutable via
|
|
57
|
+
// `board_set_active`. NEVER read by anything outside this file — every
|
|
58
|
+
// downstream call receives an explicit ctx.
|
|
59
|
+
let sessionBoardRow = null;
|
|
60
|
+
|
|
61
|
+
function setSessionBoard(row) {
|
|
62
|
+
sessionBoardRow = row;
|
|
63
|
+
if (row) touchBoardActive(row.id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getSessionBoard() {
|
|
67
|
+
return sessionBoardRow;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Walk up from `startDir` toward the filesystem root, returning the first
|
|
72
|
+
* directory that satisfies `predicate`. Returns null if none found.
|
|
73
|
+
*/
|
|
74
|
+
function walkUp(startDir, predicate) {
|
|
75
|
+
let dir = startDir;
|
|
76
|
+
while (true) {
|
|
77
|
+
if (predicate(dir)) return dir;
|
|
78
|
+
const parent = dirname(dir);
|
|
79
|
+
if (parent === dir) return null;
|
|
80
|
+
dir = parent;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Initialise the MCP session board. Resolution order:
|
|
86
|
+
* 1. Walk up from PROJECT_ROOT (cwd unless `orbit mcp --cwd` or env overrides it)
|
|
87
|
+
* for an existing `.orbit/board.db` — finds the board even when the agent
|
|
88
|
+
* is launched from a subdirectory or MCP config uses an absolute command.
|
|
89
|
+
* 2. Walk up from PROJECT_ROOT for a `.git/` directory — auto-creates a board at
|
|
90
|
+
* the git root on first launch.
|
|
91
|
+
* 3. Fall back to PROJECT_ROOT itself — supports non-git projects and global installs.
|
|
92
|
+
*/
|
|
93
|
+
function initMcpSessionBoard() {
|
|
94
|
+
const start = normalizePath(process.env.PROJECT_ROOT ? resolve(process.env.PROJECT_ROOT) : process.cwd());
|
|
95
|
+
|
|
96
|
+
// 1. Prefer the registry row for PROJECT_ROOT (or one of its ancestors). New
|
|
97
|
+
// Orbit boards live in central DATA_DIR storage, so there may be no in-repo
|
|
98
|
+
// .orbit/board.db to discover.
|
|
99
|
+
const registeredRoot = walkUp(start, (dir) => Boolean(getBoardByRepoPath(normalizePath(dir))));
|
|
100
|
+
if (registeredRoot) {
|
|
101
|
+
setSessionBoard(getBoardByRepoPath(normalizePath(registeredRoot)));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. Walk up for an existing legacy board db.
|
|
106
|
+
const boardRoot = walkUp(start, (dir) => existsSync(join(dir, ".orbit", "board.db")));
|
|
107
|
+
if (boardRoot) {
|
|
108
|
+
const root = normalizePath(boardRoot);
|
|
109
|
+
const existing = getBoardByRepoPath(root);
|
|
110
|
+
if (existing) { setSessionBoard(existing); return; }
|
|
111
|
+
// db exists on disk but isn't in the registry yet — register it.
|
|
112
|
+
const dbPath = join(boardRoot, ".orbit", "board.db");
|
|
113
|
+
const db = openConnection(dbPath);
|
|
114
|
+
createBoardSchema(db);
|
|
115
|
+
const seeded = seedIfEmpty(db, root);
|
|
116
|
+
if (seeded) {
|
|
117
|
+
const t = now();
|
|
118
|
+
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 });
|
|
119
|
+
}
|
|
120
|
+
const fresh = getBoardByRepoPath(root);
|
|
121
|
+
if (fresh) { syncRegistryFromBoardDb(fresh, db); setSessionBoard(fresh); }
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 3. Walk up for a git root; fall back to cwd for non-git projects.
|
|
126
|
+
const gitRoot = walkUp(start, (dir) => existsSync(join(dir, ".git")));
|
|
127
|
+
const result = provisionRepoBoard(normalizePath(gitRoot || start));
|
|
128
|
+
if (result.registryRow) setSessionBoard(result.registryRow);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createBoardAtRoot(root) {
|
|
132
|
+
const dbPath = join(root, ".orbit", "board.db");
|
|
133
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
134
|
+
const db = openConnection(dbPath);
|
|
135
|
+
createBoardSchema(db);
|
|
136
|
+
const seeded = seedIfEmpty(db, root);
|
|
137
|
+
if (seeded) {
|
|
138
|
+
const t = now();
|
|
139
|
+
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 });
|
|
140
|
+
}
|
|
141
|
+
const fresh = getBoardByRepoPath(root);
|
|
142
|
+
if (fresh) { syncRegistryFromBoardDb(fresh, db); setSessionBoard(fresh); }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
initMcpSessionBoard();
|
|
146
|
+
closeAllConnections();
|
|
147
|
+
|
|
148
|
+
/** Build a ctx for the session board (or a board the tool argument selected). */
|
|
149
|
+
function ctxFor(boardRow, actor) {
|
|
150
|
+
if (!boardRow) throw rpcError(-32602, "No board in session; pass board_id or board_slug, or call board_set_active first.");
|
|
151
|
+
return { actor, board: boardRow, db: openBoardDb(boardRow) };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sessionCtx(actor) {
|
|
155
|
+
return ctxFor(getSessionBoard(), actor);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function withAutomaticBackup(ctx, result) {
|
|
159
|
+
scheduleAutomaticBoardBackup(ctx.board, ctx.db);
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Resolve a registry row from explicit args (no session fallback). */
|
|
164
|
+
function resolveExplicitBoard(args) {
|
|
165
|
+
if (args.board_id) {
|
|
166
|
+
const row = getBoardByRegistryId(args.board_id);
|
|
167
|
+
if (!row) throw rpcError(-32004, "Board id not found.");
|
|
168
|
+
return row;
|
|
169
|
+
}
|
|
170
|
+
if (args.board_slug || args.board) {
|
|
171
|
+
const row = getBoardBySlug(args.board_slug || args.board);
|
|
172
|
+
if (!row) throw rpcError(-32004, "Board slug not found.");
|
|
173
|
+
return row;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Resolve a board for a tool that accepts board_id/board_slug AND can fall
|
|
179
|
+
* back to the session board. */
|
|
180
|
+
function resolveBoardOrSession(args) {
|
|
181
|
+
return resolveExplicitBoard(args) || getSessionBoard();
|
|
182
|
+
}
|
|
15
183
|
|
|
16
184
|
const TOOL_DEFS = [
|
|
17
185
|
{
|
|
@@ -75,28 +243,56 @@ const TOOL_DEFS = [
|
|
|
75
243
|
{
|
|
76
244
|
name: "board_get_ticket_context",
|
|
77
245
|
description:
|
|
78
|
-
"Read the
|
|
246
|
+
"Read the default agent context pack for a ticket, including board agent_instructions (project-level agent context), relations, blockers, implementation fields, and child cards. Ticket comments are intentionally omitted; use board_read_comments for explicit comment retrieval.",
|
|
79
247
|
inputSchema: {
|
|
80
248
|
type: "object",
|
|
81
249
|
properties: {
|
|
82
250
|
ticket_id: { type: "string" },
|
|
83
|
-
|
|
251
|
+
board_id: { type: "string" },
|
|
252
|
+
board_slug: { type: "string" },
|
|
253
|
+
board: { type: "string" },
|
|
254
|
+
depth: { type: "integer", minimum: 1, maximum: 3 },
|
|
255
|
+
max_chars_per_field: { type: "integer", minimum: 1 },
|
|
256
|
+
include_parent_full: { type: "boolean" },
|
|
257
|
+
include_related_full: { type: "boolean" }
|
|
84
258
|
},
|
|
85
259
|
required: ["ticket_id"],
|
|
86
260
|
additionalProperties: false
|
|
87
261
|
},
|
|
88
262
|
handler: (args) => orbitClient.getTicketContext(args)
|
|
89
263
|
},
|
|
264
|
+
{
|
|
265
|
+
name: "board_get_agent_dispatch_packet",
|
|
266
|
+
description:
|
|
267
|
+
"Return a lean agent dispatch packet for one ticket: board identity/instructions, capped ticket description/acceptance/AI plan, blockers, shallow parent, relevant workflow state IDs, repo path/default branch, recent capped comments, and label names. Parent/related/implementation bodies are intentionally omitted.",
|
|
268
|
+
inputSchema: {
|
|
269
|
+
type: "object",
|
|
270
|
+
properties: {
|
|
271
|
+
ticket_id: { type: "string" },
|
|
272
|
+
board_id: { type: "string" },
|
|
273
|
+
board_slug: { type: "string" },
|
|
274
|
+
board: { type: "string" },
|
|
275
|
+
max_chars_per_field: { type: "integer", minimum: 1 },
|
|
276
|
+
comment_limit: { type: "integer", minimum: 0, maximum: 20 }
|
|
277
|
+
},
|
|
278
|
+
required: ["ticket_id"],
|
|
279
|
+
additionalProperties: false
|
|
280
|
+
},
|
|
281
|
+
handler: (args) => orbitClient.getAgentDispatchPacket(args)
|
|
282
|
+
},
|
|
90
283
|
{
|
|
91
284
|
name: "board_read_ticket",
|
|
92
285
|
description:
|
|
93
|
-
"Look up a ticket by ticket_id, number, or exact title (case-insensitive) on the session's active board, and return its title, description, labels, state, and
|
|
286
|
+
"Look up a ticket by ticket_id, number, or exact title (case-insensitive) on the session's active board, and return its title, description, labels, state, and the board manual (agent_instructions + journal + deployment paths). Ticket comments are intentionally omitted; use board_read_comments for explicit comment retrieval.",
|
|
94
287
|
inputSchema: {
|
|
95
288
|
type: "object",
|
|
96
289
|
properties: {
|
|
97
290
|
ticket_id: { type: "string" },
|
|
98
291
|
number: { type: "integer" },
|
|
99
|
-
title: { type: "string" }
|
|
292
|
+
title: { type: "string" },
|
|
293
|
+
board_id: { type: "string" },
|
|
294
|
+
board_slug: { type: "string" },
|
|
295
|
+
board: { type: "string" }
|
|
100
296
|
},
|
|
101
297
|
additionalProperties: false
|
|
102
298
|
},
|
|
@@ -111,7 +307,10 @@ const TOOL_DEFS = [
|
|
|
111
307
|
properties: {
|
|
112
308
|
ticket_id: { type: "string" },
|
|
113
309
|
number: { type: "integer" },
|
|
114
|
-
title: { type: "string" }
|
|
310
|
+
title: { type: "string" },
|
|
311
|
+
board_id: { type: "string" },
|
|
312
|
+
board_slug: { type: "string" },
|
|
313
|
+
board: { type: "string" }
|
|
115
314
|
},
|
|
116
315
|
additionalProperties: false
|
|
117
316
|
},
|
|
@@ -123,7 +322,10 @@ const TOOL_DEFS = [
|
|
|
123
322
|
inputSchema: {
|
|
124
323
|
type: "object",
|
|
125
324
|
properties: {
|
|
126
|
-
ticket_id: { type: "string" }
|
|
325
|
+
ticket_id: { type: "string" },
|
|
326
|
+
board_id: { type: "string" },
|
|
327
|
+
board_slug: { type: "string" },
|
|
328
|
+
board: { type: "string" }
|
|
127
329
|
},
|
|
128
330
|
required: ["ticket_id"],
|
|
129
331
|
additionalProperties: false
|
|
@@ -136,7 +338,10 @@ const TOOL_DEFS = [
|
|
|
136
338
|
inputSchema: {
|
|
137
339
|
type: "object",
|
|
138
340
|
properties: {
|
|
139
|
-
ticket_id: { type: "string" }
|
|
341
|
+
ticket_id: { type: "string" },
|
|
342
|
+
board_id: { type: "string" },
|
|
343
|
+
board_slug: { type: "string" },
|
|
344
|
+
board: { type: "string" }
|
|
140
345
|
},
|
|
141
346
|
required: ["ticket_id"],
|
|
142
347
|
additionalProperties: false
|
|
@@ -154,7 +359,11 @@ const TOOL_DEFS = [
|
|
|
154
359
|
board: { type: "string" },
|
|
155
360
|
board_slug: { type: "string" },
|
|
156
361
|
board_id: { type: "string" },
|
|
157
|
-
limit: { type: "integer", minimum: 1, maximum: 50 }
|
|
362
|
+
limit: { type: "integer", minimum: 1, maximum: 50 },
|
|
363
|
+
mode: { type: "string", enum: ["ids", "summary", "full"] },
|
|
364
|
+
include_full: { type: "boolean" },
|
|
365
|
+
fields: { type: "array", items: { type: "string" } },
|
|
366
|
+
max_chars_per_field: { type: "integer", minimum: 1 }
|
|
158
367
|
},
|
|
159
368
|
required: ["q"],
|
|
160
369
|
additionalProperties: false
|
|
@@ -332,7 +541,9 @@ const TOOL_DEFS = [
|
|
|
332
541
|
type: "object",
|
|
333
542
|
properties: {
|
|
334
543
|
board_id: { type: "string" },
|
|
335
|
-
board_slug: { type: "string" }
|
|
544
|
+
board_slug: { type: "string" },
|
|
545
|
+
include_attachments: { type: "boolean", description: "Embed attached image bytes in the JSON snapshot." },
|
|
546
|
+
include_images: { type: "boolean", description: "Alias for include_attachments." }
|
|
336
547
|
},
|
|
337
548
|
additionalProperties: false
|
|
338
549
|
},
|
|
@@ -341,7 +552,7 @@ const TOOL_DEFS = [
|
|
|
341
552
|
{
|
|
342
553
|
name: "board_update_settings",
|
|
343
554
|
description:
|
|
344
|
-
"PATCH-equivalent for board settings: name, repo_url, system_path, default_branch, agent_instructions (project-level agent context), and project_notes (Notes For You).
|
|
555
|
+
"PATCH-equivalent for board settings: name (display rename only; slug/canonical URLs stay unchanged), repo_url, system_path, default_branch, agent_instructions (project-level agent context), and project_notes (Notes For You).",
|
|
345
556
|
inputSchema: {
|
|
346
557
|
type: "object",
|
|
347
558
|
properties: {
|
|
@@ -364,13 +575,21 @@ const tools = new Map(TOOL_DEFS.map((tool) => [tool.name, tool]));
|
|
|
364
575
|
|
|
365
576
|
let inputBuffer = Buffer.alloc(0);
|
|
366
577
|
let expectedBodyLength = -1;
|
|
578
|
+
let shuttingDown = false;
|
|
579
|
+
|
|
580
|
+
function shutdown(code = 0) {
|
|
581
|
+
if (shuttingDown) return;
|
|
582
|
+
shuttingDown = true;
|
|
583
|
+
orbitClient.close?.();
|
|
584
|
+
process.exit(code);
|
|
585
|
+
}
|
|
367
586
|
|
|
368
587
|
process.stdin.on("data", (chunk) => {
|
|
369
588
|
inputBuffer = Buffer.concat([inputBuffer, chunk]);
|
|
370
589
|
drainInput();
|
|
371
590
|
});
|
|
372
591
|
|
|
373
|
-
process.stdin.on("end", () =>
|
|
592
|
+
process.stdin.on("end", () => shutdown(0));
|
|
374
593
|
process.stdin.resume();
|
|
375
594
|
|
|
376
595
|
function drainInput() {
|
|
@@ -449,6 +668,8 @@ async function handleMessage(body, transport = "content-length") {
|
|
|
449
668
|
}, transport);
|
|
450
669
|
} catch (error) {
|
|
451
670
|
writeMessage(errorResponse(message.id, error.code || -32000, error.message || "Internal error."), transport);
|
|
671
|
+
} finally {
|
|
672
|
+
orbitClient.close?.();
|
|
452
673
|
}
|
|
453
674
|
}
|
|
454
675
|
|
|
@@ -544,6 +765,11 @@ process.on("unhandledRejection", (error) => {
|
|
|
544
765
|
writeMessage(errorResponse(null, -32000, error?.message || "Unhandled rejection."));
|
|
545
766
|
});
|
|
546
767
|
|
|
768
|
+
process.once("SIGINT", () => shutdown(0));
|
|
769
|
+
process.once("SIGTERM", () => shutdown(0));
|
|
770
|
+
process.once("exit", () => orbitClient.close?.());
|
|
771
|
+
|
|
547
772
|
if (process.env.MAB_MCP_STDERR_LOG === "1") {
|
|
548
|
-
|
|
773
|
+
const target = typeof orbitClient.sessionLabel === "function" ? orbitClient.sessionLabel() : orbitClient.sessionLabel;
|
|
774
|
+
console.error(`Starscape Orbit MCP ready. Mode: ${orbitClient.mode}. Target: ${target}`);
|
|
549
775
|
}
|
package/src/server.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
startSSEStream,
|
|
9
9
|
startupSummary
|
|
10
10
|
} from "./core/board.js";
|
|
11
|
+
import { closeAllConnections } from "./core/db.js";
|
|
11
12
|
|
|
12
13
|
const server = createServer(async (req, res) => {
|
|
13
14
|
try {
|
|
@@ -46,3 +47,22 @@ server.listen(boardRuntime.port, () => {
|
|
|
46
47
|
console.log(`Boards (${summary.boardCount}):`);
|
|
47
48
|
for (const path of summary.boardPaths) console.log(` - ${path}`);
|
|
48
49
|
});
|
|
50
|
+
|
|
51
|
+
let shuttingDown = false;
|
|
52
|
+
|
|
53
|
+
function shutdown() {
|
|
54
|
+
if (shuttingDown) return;
|
|
55
|
+
shuttingDown = true;
|
|
56
|
+
server.close(() => {
|
|
57
|
+
closeAllConnections();
|
|
58
|
+
process.exit(0);
|
|
59
|
+
});
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
closeAllConnections();
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}, 1500).unref();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
process.once("SIGINT", shutdown);
|
|
67
|
+
process.once("SIGTERM", shutdown);
|
|
68
|
+
process.once("exit", () => closeAllConnections());
|