@starlove/orbit 1.3.4 → 1.4.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/Dockerfile +2 -1
- package/README.md +10 -9
- package/SKILL-ORBIT.md +41 -17
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/OFL.txt +93 -0
- package/dist/full/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/dist/full/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/dist/full/fonts/Inter/OFL.txt +93 -0
- package/dist/full/fonts/Inter/README.txt +118 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/dist/full/fonts/JetBrains_Mono/OFL.txt +93 -0
- package/dist/full/fonts/JetBrains_Mono/README.txt +79 -0
- package/dist/full/fonts/OpenRing-Regular.ttf +0 -0
- package/dist/full/fonts/Open_Sans/OFL.txt +93 -0
- package/dist/full/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/dist/full/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/dist/full/fonts/Open_Sans/README.txt +100 -0
- package/dist/full/fonts/Orbitron/OFL.txt +93 -0
- package/dist/full/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/dist/full/fonts/Orbitron/README.txt +68 -0
- package/dist/full/fonts/Rajdhani/OFL.txt +93 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/dist/full/fonts/runescape-uf.ttf +0 -0
- package/dist/full/index.html +137 -27
- package/dist/full/js/app.62905fcb.js +952 -0
- package/dist/full/js/app.62905fcb.js.map +7 -0
- package/dist/full/styles.4e60d824.css +1 -0
- package/dist/full/version.json +2 -2
- package/docs/AGENT_PROTOCOL.md +20 -13
- package/docs/ORBIT_DISPATCH.md +73 -38
- package/docs/UPDATING.md +114 -0
- package/package.json +5 -7
- package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/OFL.txt +93 -0
- package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/OFL.txt +93 -0
- package/public/fonts/Inter/README.txt +118 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/OFL.txt +93 -0
- package/public/fonts/JetBrains_Mono/README.txt +79 -0
- package/public/fonts/OpenRing-Regular.ttf +0 -0
- package/public/fonts/Open_Sans/OFL.txt +93 -0
- package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/README.txt +100 -0
- package/public/fonts/Orbitron/OFL.txt +93 -0
- package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/public/fonts/Orbitron/README.txt +68 -0
- package/public/fonts/Rajdhani/OFL.txt +93 -0
- package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/public/fonts/runescape-uf.ttf +0 -0
- package/public/index.html +135 -25
- package/public/js/artifact-drawer.js +79 -0
- package/public/js/board-export.js +161 -0
- package/public/js/board-menu.js +219 -17
- package/public/js/card-actions.js +1 -1
- package/public/js/create-card.js +60 -23
- package/public/js/drawer.js +164 -4
- package/public/js/font-preference.js +229 -0
- package/public/js/format.js +0 -10
- package/public/js/icons.js +16 -0
- package/public/js/kanban.js +123 -1
- package/public/js/local-backend.js +31 -4
- package/public/js/main.js +7 -1
- package/public/js/modal.js +3 -1
- package/public/js/panel.js +58 -0
- package/public/js/settings-tabs.js +1 -1
- package/public/js/settings.js +606 -275
- package/public/js/sse.js +3 -0
- package/public/js/state.js +1 -1
- package/public/js/text-editing.js +128 -0
- package/public/js/ticket-detail.js +377 -148
- package/public/js/ticket-typeahead.js +49 -0
- package/public/js/toast.js +19 -4
- package/public/js/topbar-notes.js +3 -3
- package/public/js/typeahead.js +206 -0
- package/public/js/update-check.js +58 -0
- package/public/styles.css +5455 -3538
- package/src/cli/orbit.js +9 -5
- package/src/core/agent.js +12 -4
- package/src/core/agents-md.js +3 -3
- package/src/core/artifacts.js +149 -0
- package/src/core/board.js +3 -2
- package/src/core/boards.js +0 -1
- package/src/core/css-bundle.js +1 -0
- package/src/core/db.js +6 -0
- package/src/core/dispatch.js +344 -34
- package/src/core/http.js +9 -2
- package/src/core/paths.js +26 -2
- package/src/core/router.js +33 -0
- package/src/core/search.js +3 -2
- package/src/core/snapshots.js +10 -5
- package/src/core/tickets.js +34 -3
- package/src/core/util.js +3 -2
- package/src/core/version-check.js +154 -0
- package/src/mcp/orbit-client.js +18 -2
- package/src/mcp-server.js +19 -14
- package/src/server.js +28 -3
- package/dist/full/js/app.fb7767dc.js +0 -742
- package/dist/full/js/app.fb7767dc.js.map +0 -7
- package/dist/full/styles.bbc542f5.css +0 -1
package/src/core/snapshots.js
CHANGED
|
@@ -75,7 +75,7 @@ export function exportBoard(boardId, ctx, options = {}) {
|
|
|
75
75
|
|
|
76
76
|
return {
|
|
77
77
|
format: "orbit-board-export",
|
|
78
|
-
version:
|
|
78
|
+
version: 9,
|
|
79
79
|
exported_at: now(),
|
|
80
80
|
include_attachments: Boolean(options.includeAttachments),
|
|
81
81
|
board: { ...innerBoard },
|
|
@@ -168,9 +168,10 @@ export function importBoardSnapshot(body, ctx) {
|
|
|
168
168
|
db.prepare(
|
|
169
169
|
`INSERT INTO tickets
|
|
170
170
|
(id, board_id, number, title, description, type, parent_ticket_id, ai_plan,
|
|
171
|
-
|
|
171
|
+
plan_artifact_path, implementation_summary, implementation_artifact_path,
|
|
172
|
+
implementation_updates, state_id, priority,
|
|
172
173
|
created_by, created_at, updated_at, archived_at)
|
|
173
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
174
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
174
175
|
).run(
|
|
175
176
|
ticket.id,
|
|
176
177
|
targetBoardId,
|
|
@@ -180,7 +181,9 @@ export function importBoardSnapshot(body, ctx) {
|
|
|
180
181
|
normalizeTicketType(ticket.type || "task"),
|
|
181
182
|
ticket.parent_ticket_id || null,
|
|
182
183
|
ticket.ai_plan || "",
|
|
184
|
+
ticket.plan_artifact_path || "",
|
|
183
185
|
ticket.implementation_summary || "",
|
|
186
|
+
ticket.implementation_artifact_path || "",
|
|
184
187
|
ticket.implementation_updates || "",
|
|
185
188
|
ticket.state_id,
|
|
186
189
|
normalizePriority(ticket.priority),
|
|
@@ -197,13 +200,14 @@ export function importBoardSnapshot(body, ctx) {
|
|
|
197
200
|
|
|
198
201
|
for (const comment of snapshot.comments || []) {
|
|
199
202
|
db.prepare(
|
|
200
|
-
"INSERT INTO comments (id, ticket_id, author, kind, body, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
203
|
+
"INSERT INTO comments (id, ticket_id, author, kind, body, omitted_for_ai, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
201
204
|
).run(
|
|
202
205
|
comment.id,
|
|
203
206
|
comment.ticket_id,
|
|
204
207
|
comment.author || "import",
|
|
205
208
|
comment.kind || "comment",
|
|
206
209
|
comment.body || "",
|
|
210
|
+
comment.omitted_for_ai ? 1 : 0,
|
|
207
211
|
comment.created_at || now()
|
|
208
212
|
);
|
|
209
213
|
}
|
|
@@ -463,6 +467,7 @@ function trelloExportToOrbitSnapshot(trello, ctx) {
|
|
|
463
467
|
author: trelloMemberName(action.memberCreator),
|
|
464
468
|
kind: "human_comment",
|
|
465
469
|
body: cleanText(action.data?.text) || "",
|
|
470
|
+
omitted_for_ai: 0,
|
|
466
471
|
created_at: cleanText(action.date) || cardUpdatedAt
|
|
467
472
|
});
|
|
468
473
|
});
|
|
@@ -471,7 +476,7 @@ function trelloExportToOrbitSnapshot(trello, ctx) {
|
|
|
471
476
|
return {
|
|
472
477
|
format: "orbit-board-export",
|
|
473
478
|
source_format: "trello-board-export",
|
|
474
|
-
version:
|
|
479
|
+
version: 9,
|
|
475
480
|
exported_at: time,
|
|
476
481
|
include_attachments: false,
|
|
477
482
|
imported_counts: {
|
package/src/core/tickets.js
CHANGED
|
@@ -62,8 +62,9 @@ export function createTicket(body, ctx) {
|
|
|
62
62
|
db.prepare(
|
|
63
63
|
`INSERT INTO tickets
|
|
64
64
|
(id, board_id, number, title, description, type, parent_ticket_id, ai_plan,
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
plan_artifact_path, implementation_summary, implementation_artifact_path,
|
|
66
|
+
implementation_updates, state_id, priority, created_by, created_at, updated_at)
|
|
67
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
67
68
|
).run(
|
|
68
69
|
ticketId,
|
|
69
70
|
boardId,
|
|
@@ -73,7 +74,9 @@ export function createTicket(body, ctx) {
|
|
|
73
74
|
ticketType,
|
|
74
75
|
parentTicketId,
|
|
75
76
|
body.ai_plan || "",
|
|
77
|
+
body.plan_artifact_path || "",
|
|
76
78
|
body.implementation_summary || "",
|
|
79
|
+
body.implementation_artifact_path || "",
|
|
77
80
|
body.implementation_updates || "",
|
|
78
81
|
stateId,
|
|
79
82
|
normalizePriority(body.priority),
|
|
@@ -113,7 +116,9 @@ export function updateTicket(ticketId, body, ctx) {
|
|
|
113
116
|
"type",
|
|
114
117
|
"parent_ticket_id",
|
|
115
118
|
"ai_plan",
|
|
119
|
+
"plan_artifact_path",
|
|
116
120
|
"implementation_summary",
|
|
121
|
+
"implementation_artifact_path",
|
|
117
122
|
"implementation_updates",
|
|
118
123
|
"state_id",
|
|
119
124
|
"priority"
|
|
@@ -295,6 +300,32 @@ export function createComment(ticketId, body, ctx) {
|
|
|
295
300
|
});
|
|
296
301
|
}
|
|
297
302
|
|
|
303
|
+
export function updateCommentAiOmission(ticketId, commentId, body, ctx) {
|
|
304
|
+
const { db, board, actor } = ctx;
|
|
305
|
+
const ticket = ticketById(db, ticketId);
|
|
306
|
+
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
307
|
+
requireBoardAccess(actor, boardById(db, ticket.board_id));
|
|
308
|
+
requirePermission(actor, "write");
|
|
309
|
+
|
|
310
|
+
const comment = db.prepare("SELECT * FROM comments WHERE id = ? AND ticket_id = ?").get(commentId, ticketId);
|
|
311
|
+
if (!comment) throw httpError(404, "comment_not_found");
|
|
312
|
+
const omittedForAi = body.omitted_for_ai ?? body.omittedForAi;
|
|
313
|
+
if (typeof omittedForAi !== "boolean") throw httpError(400, "omitted_for_ai_required");
|
|
314
|
+
|
|
315
|
+
return tx(db, () => {
|
|
316
|
+
const time = now();
|
|
317
|
+
db.prepare("UPDATE comments SET omitted_for_ai = ? WHERE id = ?").run(omittedForAi ? 1 : 0, commentId);
|
|
318
|
+
db.prepare("UPDATE tickets SET updated_at = ? WHERE id = ?").run(time, ticketId);
|
|
319
|
+
recordEvent(db, ticket.board_id, "comment_ai_omission_changed", ticketId, actor.name, {
|
|
320
|
+
comment_id: commentId,
|
|
321
|
+
omitted_for_ai: omittedForAi
|
|
322
|
+
});
|
|
323
|
+
reindexTicket(db, ticketId);
|
|
324
|
+
bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
|
|
325
|
+
return db.prepare("SELECT * FROM comments WHERE id = ?").get(commentId);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
298
329
|
export function getTicketStatusHistory(ticketId, ctx) {
|
|
299
330
|
const { db, board, actor } = ctx;
|
|
300
331
|
const ticket = ticketById(db, ticketId);
|
|
@@ -342,7 +373,7 @@ export function reindexTicket(db, ticketId) {
|
|
|
342
373
|
const ticket = db.prepare("SELECT * FROM tickets WHERE id = ?").get(ticketId);
|
|
343
374
|
if (!ticket) return;
|
|
344
375
|
const comments = db
|
|
345
|
-
.prepare("SELECT body FROM comments WHERE ticket_id = ? ORDER BY created_at ASC")
|
|
376
|
+
.prepare("SELECT body FROM comments WHERE ticket_id = ? AND COALESCE(omitted_for_ai, 0) = 0 ORDER BY created_at ASC")
|
|
346
377
|
.all(ticketId)
|
|
347
378
|
.map((comment) => comment.body)
|
|
348
379
|
.join("\n");
|
package/src/core/util.js
CHANGED
|
@@ -87,10 +87,11 @@ export function normalizeStateRole(value) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export function appendFieldNote(existing, note, actorName) {
|
|
90
|
+
void actorName;
|
|
90
91
|
const text = String(note || "").trim();
|
|
91
92
|
if (!text) return existing || "";
|
|
92
|
-
const
|
|
93
|
-
return [
|
|
93
|
+
const prior = String(existing || "").trim();
|
|
94
|
+
return [prior, text].filter(Boolean).join("\n\n");
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
export function toFtsQuery(q) {
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Update checking. The server reads its own version from package.json and
|
|
2
|
+
// compares it against the `version` field of package.json on the project's
|
|
3
|
+
// GitHub default branch. We hit the GitHub *contents API* (not the raw CDN):
|
|
4
|
+
// it honours conditional requests / no-cache so the answer is authoritative,
|
|
5
|
+
// and because only the server polls — not every browser — GitHub's 60 req/hr
|
|
6
|
+
// unauthenticated limit is a non-issue. Results are cached in-memory for
|
|
7
|
+
// ORBIT_UPDATE_TTL minutes; `force` bypasses the cache for a "Check now".
|
|
8
|
+
//
|
|
9
|
+
// Env:
|
|
10
|
+
// ORBIT_UPDATE_CHECK=0 disable the check entirely (returns enabled:false)
|
|
11
|
+
// ORBIT_UPDATE_TTL=<min> cache lifetime, default 60
|
|
12
|
+
// ORBIT_UPDATE_BRANCH branch to read, default the repo default (master)
|
|
13
|
+
// ORBIT_GITHUB_TOKEN optional PAT (private repos / higher rate limit)
|
|
14
|
+
// ORBIT_SELF_UPDATE=1 enable the gated POST /api/update self-update
|
|
15
|
+
// ORBIT_DOCKER=1 force "docker" deploy mode
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
import { ROOT_DIR, DATA_DIR } from "./paths.js";
|
|
21
|
+
|
|
22
|
+
const DEFAULT_OWNER = "brucestarlove";
|
|
23
|
+
const DEFAULT_REPO = "ss-orbit";
|
|
24
|
+
const CHECK_TIMEOUT_MS = 5000;
|
|
25
|
+
|
|
26
|
+
let pkgCache = null;
|
|
27
|
+
function readPackage() {
|
|
28
|
+
if (pkgCache) return pkgCache;
|
|
29
|
+
try {
|
|
30
|
+
pkgCache = JSON.parse(readFileSync(join(ROOT_DIR, "package.json"), "utf8"));
|
|
31
|
+
} catch {
|
|
32
|
+
pkgCache = {};
|
|
33
|
+
}
|
|
34
|
+
return pkgCache;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Parse owner/repo out of package.json repository.url, falling back to the
|
|
38
|
+
* known Orbit repo so the check still works on odd checkouts. */
|
|
39
|
+
function repoCoordinates() {
|
|
40
|
+
const url = readPackage()?.repository?.url || "";
|
|
41
|
+
const match = url.match(/github\.com[/:]([^/]+)\/([^/.]+)/i);
|
|
42
|
+
return { owner: match?.[1] || DEFAULT_OWNER, repo: match?.[2] || DEFAULT_REPO };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function localVersion() {
|
|
46
|
+
return readPackage()?.version || "0.0.0";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function updateBranch() {
|
|
50
|
+
return process.env.ORBIT_UPDATE_BRANCH || "master";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** How this instance is running, which decides the update instructions the
|
|
54
|
+
* client shows. Docker images carry no git tree, so they're rebuild-only. */
|
|
55
|
+
function detectDeployMode() {
|
|
56
|
+
if (process.env.ORBIT_DOCKER === "1" || existsSync("/.dockerenv")) return "docker";
|
|
57
|
+
if (existsSync(join(ROOT_DIR, ".git"))) return "source";
|
|
58
|
+
return "dist";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Numeric "a.b.c" comparison (ignores any prerelease suffix). Returns true
|
|
62
|
+
* when `latest` is strictly newer than `current`. */
|
|
63
|
+
function isNewer(latest, current) {
|
|
64
|
+
const parse = (v) => String(v).replace(/^v/, "").split(/[.+-]/).map((n) => Number(n) || 0);
|
|
65
|
+
const a = parse(latest);
|
|
66
|
+
const b = parse(current);
|
|
67
|
+
for (let i = 0; i < 3; i += 1) {
|
|
68
|
+
if ((a[i] || 0) > (b[i] || 0)) return true;
|
|
69
|
+
if ((a[i] || 0) < (b[i] || 0)) return false;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function fetchLatestVersion() {
|
|
75
|
+
const { owner, repo } = repoCoordinates();
|
|
76
|
+
const branch = updateBranch();
|
|
77
|
+
const headers = {
|
|
78
|
+
Accept: "application/vnd.github.raw+json",
|
|
79
|
+
"User-Agent": "orbit-update-check"
|
|
80
|
+
};
|
|
81
|
+
if (process.env.ORBIT_GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.ORBIT_GITHUB_TOKEN}`;
|
|
82
|
+
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const timer = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
|
|
85
|
+
try {
|
|
86
|
+
const res = await fetch(
|
|
87
|
+
`https://api.github.com/repos/${owner}/${repo}/contents/package.json?ref=${encodeURIComponent(branch)}`,
|
|
88
|
+
{ headers, signal: controller.signal, cache: "no-store" }
|
|
89
|
+
);
|
|
90
|
+
if (res.status === 403 || res.status === 429) return { error: "rate_limited" };
|
|
91
|
+
if (!res.ok) return { error: `http_${res.status}` };
|
|
92
|
+
const pkg = JSON.parse(await res.text());
|
|
93
|
+
if (!pkg?.version) return { error: "no_version" };
|
|
94
|
+
return { latest: String(pkg.version) };
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return { error: err?.name === "AbortError" ? "timeout" : "unreachable" };
|
|
97
|
+
} finally {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let cache = null; // { at: epochMs, payload }
|
|
103
|
+
|
|
104
|
+
function staticPayload(extra) {
|
|
105
|
+
const { owner, repo } = repoCoordinates();
|
|
106
|
+
const branch = updateBranch();
|
|
107
|
+
return {
|
|
108
|
+
current: localVersion(),
|
|
109
|
+
deployMode: detectDeployMode(),
|
|
110
|
+
selfUpdate: process.env.ORBIT_SELF_UPDATE === "1",
|
|
111
|
+
repoUrl: `https://github.com/${owner}/${repo}`,
|
|
112
|
+
changesUrl: `https://github.com/${owner}/${repo}/commits/${branch}`,
|
|
113
|
+
...extra
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Get current/latest version info, cached for ORBIT_UPDATE_TTL minutes.
|
|
118
|
+
* Never throws — network problems surface as `{ error, updateAvailable:false }`. */
|
|
119
|
+
export async function getVersionInfo({ force = false } = {}) {
|
|
120
|
+
if (process.env.ORBIT_UPDATE_CHECK === "0") {
|
|
121
|
+
return staticPayload({ enabled: false, latest: null, updateAvailable: false, error: null, checkedAt: null });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const ttlMs = (Number(process.env.ORBIT_UPDATE_TTL) || 60) * 60 * 1000;
|
|
125
|
+
if (!force && cache && Date.now() - cache.at < ttlMs) return cache.payload;
|
|
126
|
+
|
|
127
|
+
const result = await fetchLatestVersion();
|
|
128
|
+
const current = localVersion();
|
|
129
|
+
const payload = staticPayload({
|
|
130
|
+
enabled: true,
|
|
131
|
+
latest: result.latest ?? null,
|
|
132
|
+
updateAvailable: Boolean(result.latest) && isNewer(result.latest, current),
|
|
133
|
+
error: result.error ?? null,
|
|
134
|
+
checkedAt: new Date().toISOString()
|
|
135
|
+
});
|
|
136
|
+
cache = { at: Date.now(), payload };
|
|
137
|
+
return payload;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Kick off scripts/update.sh detached and hand back immediately. Only ever
|
|
141
|
+
* reached when ORBIT_SELF_UPDATE=1 (the router gates the route). The script
|
|
142
|
+
* rebuilds and then signals this process so a supervisor restarts it. */
|
|
143
|
+
export function startSelfUpdate() {
|
|
144
|
+
const script = join(ROOT_DIR, "scripts", "update.sh");
|
|
145
|
+
if (!existsSync(script)) return { started: false, error: "script_missing" };
|
|
146
|
+
const child = spawn("bash", [script], {
|
|
147
|
+
cwd: ROOT_DIR,
|
|
148
|
+
detached: true,
|
|
149
|
+
stdio: "ignore",
|
|
150
|
+
env: { ...process.env, ORBIT_REPO_DIR: ROOT_DIR, ORBIT_SERVER_PID: String(process.pid), ORBIT_DATA_DIR: DATA_DIR }
|
|
151
|
+
});
|
|
152
|
+
child.unref();
|
|
153
|
+
return { started: true };
|
|
154
|
+
}
|
package/src/mcp/orbit-client.js
CHANGED
|
@@ -61,6 +61,13 @@ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.
|
|
|
61
61
|
const { board_id, board_slug, board, ...rest } = args;
|
|
62
62
|
return rest;
|
|
63
63
|
};
|
|
64
|
+
const settingsPatch = (args = {}) => {
|
|
65
|
+
const patch = {};
|
|
66
|
+
for (const field of ["name", "repo_url", "system_path", "default_branch", "agent_instructions"]) {
|
|
67
|
+
if (args[field] !== undefined) patch[field] = args[field];
|
|
68
|
+
}
|
|
69
|
+
return patch;
|
|
70
|
+
};
|
|
64
71
|
const searchQuery = (args = {}) => ({
|
|
65
72
|
q: args.q,
|
|
66
73
|
limit: args.limit,
|
|
@@ -119,6 +126,7 @@ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.
|
|
|
119
126
|
async search(args = {}) { return request("GET", "/api/search", { query: searchQuery(args) }); },
|
|
120
127
|
async createTicket(args = {}) { return request("POST", "/api/tickets", { body: { ...args, board: requireBoard(args) }, ok: [201] }); },
|
|
121
128
|
async updateTicket({ ticket_id, ...patch }) { return request("PATCH", `/api/tickets/${encodeURIComponent(ticket_id)}`, { query: ticketQuery(patch), body: stripBoardSelectors(patch) }); },
|
|
129
|
+
async uploadMarkdownArtifact({ ticket_id, ...body }) { return request("POST", `/api/tickets/${encodeURIComponent(ticket_id)}/artifacts/markdown`, { query: ticketQuery(body), body: stripBoardSelectors(body), ok: [201] }); },
|
|
122
130
|
async addComment({ ticket_id, ...body }) { return request("POST", `/api/tickets/${encodeURIComponent(ticket_id)}/comments`, { query: ticketQuery(body), body: stripBoardSelectors(body), ok: [201] }); },
|
|
123
131
|
async createReviewVerdict({ ticket_id, ...body }) { return request("POST", `/api/tickets/${encodeURIComponent(ticket_id)}/review-verdicts`, { query: ticketQuery(body), body: stripBoardSelectors(body), ok: [201] }); },
|
|
124
132
|
async listReviewVerdicts(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/review-verdicts`, { query: { ...ticketQuery(args), limit: args.limit } }); },
|
|
@@ -135,7 +143,7 @@ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.
|
|
|
135
143
|
query: { include_attachments: args.include_attachments || args.include_images ? "true" : undefined }
|
|
136
144
|
});
|
|
137
145
|
},
|
|
138
|
-
async updateSettings(args = {}) {
|
|
146
|
+
async updateSettings(args = {}) { return request("PATCH", `/api/boards/${encodeURIComponent(requireBoard(args))}`, { body: settingsPatch(args) }); }
|
|
139
147
|
};
|
|
140
148
|
}
|
|
141
149
|
|
|
@@ -226,6 +234,13 @@ export async function createLocalOrbitClient(env = process.env) {
|
|
|
226
234
|
const { board_id, board_slug, board: _board, ...rest } = args;
|
|
227
235
|
return rest;
|
|
228
236
|
};
|
|
237
|
+
const settingsPatch = (args = {}) => {
|
|
238
|
+
const patch = {};
|
|
239
|
+
for (const field of ["name", "repo_url", "system_path", "default_branch", "agent_instructions"]) {
|
|
240
|
+
if (args[field] !== undefined) patch[field] = args[field];
|
|
241
|
+
}
|
|
242
|
+
return patch;
|
|
243
|
+
};
|
|
229
244
|
const actor = () => board.localAgentActor();
|
|
230
245
|
|
|
231
246
|
return {
|
|
@@ -249,6 +264,7 @@ export async function createLocalOrbitClient(env = process.env) {
|
|
|
249
264
|
search: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.searchTickets(args, ctx); },
|
|
250
265
|
createTicket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); const body = stripBoardSelectors(args); body.board_id = ctx.board.id; return backup(ctx, board.createTicket(body, ctx)); },
|
|
251
266
|
updateTicket: ({ ticket_id, ...patch }) => { const ctx = ctxFor(rowOrSession(patch), actor()); return backup(ctx, board.updateTicket(ticket_id, stripBoardSelectors(patch), ctx)); },
|
|
267
|
+
uploadMarkdownArtifact: ({ ticket_id, ...body }) => { const ctx = ctxFor(rowOrSession(body), actor()); return board.storeTicketMarkdownArtifact(ticket_id, stripBoardSelectors(body), ctx); },
|
|
252
268
|
addComment: ({ ticket_id, ...body }) => { const ctx = ctxFor(rowOrSession(body), actor()); return backup(ctx, board.createComment(ticket_id, stripBoardSelectors(body), ctx)); },
|
|
253
269
|
createReviewVerdict: ({ ticket_id, ...body }) => { const ctx = ctxFor(rowOrSession(body), actor()); return backup(ctx, board.createReviewVerdict(ticket_id, stripBoardSelectors(body), ctx)); },
|
|
254
270
|
listReviewVerdicts: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return { review_verdicts: board.listReviewVerdicts(args.ticket_id, ctx, args) }; },
|
|
@@ -266,6 +282,6 @@ export async function createLocalOrbitClient(env = process.env) {
|
|
|
266
282
|
includeAttachments: Boolean(args.include_attachments || args.include_images)
|
|
267
283
|
});
|
|
268
284
|
},
|
|
269
|
-
updateSettings: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor());
|
|
285
|
+
updateSettings: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.updateBoard(ctx.board.id, settingsPatch(args), ctx)); }
|
|
270
286
|
};
|
|
271
287
|
}
|
package/src/mcp-server.js
CHANGED
|
@@ -187,7 +187,7 @@ const TOOL_DEFS = [
|
|
|
187
187
|
{
|
|
188
188
|
name: "board_context",
|
|
189
189
|
description:
|
|
190
|
-
"Board/project context pack: board metadata, agent_instructions, journal entries (board_entries), and deployment paths. Complements the lean board_get_ticket_context. Resolves board from board_id / board_slug / board first; the MCP session board is only a fallback convenience. Set include_struck true to include struck journal rows (default false). Same payload as GET /api/boards/:id/context.",
|
|
190
|
+
"Board/project context pack: agent-safe board metadata, agent_instructions, journal entries (board_entries), and deployment paths. Human-only project_notes are intentionally excluded. Complements the lean board_get_ticket_context. Resolves board from board_id / board_slug / board first; the MCP session board is only a fallback convenience. Set include_struck true to include struck journal rows (default false). Same payload as GET /api/boards/:id/context.",
|
|
191
191
|
inputSchema: {
|
|
192
192
|
type: "object",
|
|
193
193
|
properties: {
|
|
@@ -287,7 +287,7 @@ const TOOL_DEFS = [
|
|
|
287
287
|
{
|
|
288
288
|
name: "board_get_agent_dispatch_packet",
|
|
289
289
|
description:
|
|
290
|
-
"Return a lean agent dispatch packet for one ticket: board identity/instructions, capped
|
|
290
|
+
"Return a lean agent dispatch packet for one ticket: board identity/instructions, capped description/acceptance, human-visible card fields and linked artifact paths, blockers, shallow parent, relevant workflow state IDs, repo path/default branch, recent capped comments, and label names. Resolves board from board_id / board_slug / board first; the MCP session board is only a fallback convenience. Full parent/related bodies and full comments are intentionally omitted.",
|
|
291
291
|
inputSchema: {
|
|
292
292
|
type: "object",
|
|
293
293
|
properties: {
|
|
@@ -407,9 +407,11 @@ const TOOL_DEFS = [
|
|
|
407
407
|
description: { type: "string" },
|
|
408
408
|
type: { type: "string", enum: ["epic", "feature", "task", "bug"] },
|
|
409
409
|
parent_ticket_id: { type: ["string", "null"] },
|
|
410
|
-
ai_plan: { type: "string" },
|
|
411
|
-
|
|
412
|
-
|
|
410
|
+
ai_plan: { type: "string", description: "Existing AI Plan field: terse human-visible plan summary; link full markdown handoffs with plan_artifact_path." },
|
|
411
|
+
plan_artifact_path: { type: "string", description: "Human-editable link/path to a full markdown/text plan artifact, e.g. docs/plan.md or ./docs/plan.md. Keep AI Plan terse; do not paste full handoffs into ai_plan." },
|
|
412
|
+
implementation_summary: { type: "string", description: "Existing Work Summary field: terse human-visible result summary; link full reports with implementation_artifact_path." },
|
|
413
|
+
implementation_artifact_path: { type: "string", description: "Human-editable link/path to a full markdown/text implementation report or evidence artifact, e.g. docs/report.md or ./docs/report.md. Keep Work Summary terse." },
|
|
414
|
+
implementation_updates: { type: "string", description: "Revisions field: terse later changes/corrections after initial implementation; omit logs, chronology, and normal completion notes." },
|
|
413
415
|
state_id: { type: "string" },
|
|
414
416
|
priority: { type: "integer" },
|
|
415
417
|
labels: { type: "array", items: { type: "string" } }
|
|
@@ -422,7 +424,7 @@ const TOOL_DEFS = [
|
|
|
422
424
|
{
|
|
423
425
|
name: "board_update_ticket",
|
|
424
426
|
description:
|
|
425
|
-
"Update ticket fields such as AI
|
|
427
|
+
"Update ticket fields such as terse AI Plan / Work Summary text, linked artifact paths, Revisions, state, type, priority, or labels (full replace: array of label names). Resolves board from board_id / board_slug / board; the MCP session board is only a fallback convenience.",
|
|
426
428
|
inputSchema: {
|
|
427
429
|
type: "object",
|
|
428
430
|
properties: {
|
|
@@ -434,9 +436,11 @@ const TOOL_DEFS = [
|
|
|
434
436
|
description: { type: "string" },
|
|
435
437
|
type: { type: "string" },
|
|
436
438
|
parent_ticket_id: { type: ["string", "null"] },
|
|
437
|
-
ai_plan: { type: "string" },
|
|
438
|
-
|
|
439
|
-
|
|
439
|
+
ai_plan: { type: "string", description: "Existing AI Plan field: terse human-visible plan summary; link full markdown handoffs with plan_artifact_path." },
|
|
440
|
+
plan_artifact_path: { type: "string", description: "Human-editable link/path to a full markdown/text plan artifact, e.g. docs/plan.md or ./docs/plan.md. Keep AI Plan terse; do not paste full handoffs into ai_plan." },
|
|
441
|
+
implementation_summary: { type: "string", description: "Existing Work Summary field: terse human-visible result summary; link full reports with implementation_artifact_path." },
|
|
442
|
+
implementation_artifact_path: { type: "string", description: "Human-editable link/path to a full markdown/text implementation report or evidence artifact, e.g. docs/report.md or ./docs/report.md. Keep Work Summary terse." },
|
|
443
|
+
implementation_updates: { type: "string", description: "Revisions field: terse later changes/corrections after initial implementation; omit logs, chronology, and normal completion notes." },
|
|
440
444
|
state_id: { type: "string" },
|
|
441
445
|
priority: { type: "integer" },
|
|
442
446
|
labels: { type: "array", items: { type: "string" }, description: "Replaces all ticket labels when provided." }
|
|
@@ -562,7 +566,7 @@ const TOOL_DEFS = [
|
|
|
562
566
|
{
|
|
563
567
|
name: "board_complete",
|
|
564
568
|
description:
|
|
565
|
-
"Hand off finished work for human review: moves the ticket to the Review lane (by role or name), writes a completion comment
|
|
569
|
+
"Hand off finished work for human review: moves the ticket to the Review lane (by role or name), writes a compact completion comment, stores summary as the terse Work Summary (`implementation_summary`), and accepts optional linked implementation_artifact_path. Pass updates only for Revisions: terse later changes/corrections after initial implementation, not normal completion notes. This is the usual 'ready for you to look' path; use board_checkpoint only when blocked mid-flight. Resolves board from board_id / board_slug / board; the MCP session board is only a fallback convenience.",
|
|
566
570
|
inputSchema: {
|
|
567
571
|
type: "object",
|
|
568
572
|
properties: {
|
|
@@ -570,8 +574,10 @@ const TOOL_DEFS = [
|
|
|
570
574
|
board_id: { type: "string" },
|
|
571
575
|
board_slug: { type: "string" },
|
|
572
576
|
board: { type: "string" },
|
|
573
|
-
summary: { type: "string" },
|
|
574
|
-
updates: { type: "string" },
|
|
577
|
+
summary: { type: "string", description: "Terse human-visible Work Summary stored in implementation_summary." },
|
|
578
|
+
updates: { type: "string", description: "Optional Revisions text only: terse later changes/corrections after initial implementation." },
|
|
579
|
+
implementation_artifact_path: { type: "string", description: "Human-editable link/path to a markdown/text implementation report artifact; can point at a report generated outside Orbit." },
|
|
580
|
+
summary_artifact_path: { type: "string", description: "Compatibility alias for implementation_artifact_path." },
|
|
575
581
|
pr_url: { type: "string" },
|
|
576
582
|
next_state: { type: "string" }
|
|
577
583
|
},
|
|
@@ -647,7 +653,7 @@ const TOOL_DEFS = [
|
|
|
647
653
|
{
|
|
648
654
|
name: "board_update_settings",
|
|
649
655
|
description:
|
|
650
|
-
"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)
|
|
656
|
+
"PATCH-equivalent for board settings: name (display rename only; slug/canonical URLs stay unchanged), repo_url, system_path, default_branch, and agent_instructions (project-level agent context). Resolves board from board_id / board_slug / board; the MCP session board is only a fallback convenience.",
|
|
651
657
|
inputSchema: {
|
|
652
658
|
type: "object",
|
|
653
659
|
properties: {
|
|
@@ -658,7 +664,6 @@ const TOOL_DEFS = [
|
|
|
658
664
|
repo_url: { type: "string" },
|
|
659
665
|
system_path: { type: "string" },
|
|
660
666
|
default_branch: { type: "string" },
|
|
661
|
-
project_notes: { type: "string" },
|
|
662
667
|
agent_instructions: { type: "string" }
|
|
663
668
|
},
|
|
664
669
|
additionalProperties: false
|
package/src/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { networkInterfaces } from "node:os";
|
|
2
3
|
import {
|
|
3
4
|
boardRuntime,
|
|
4
5
|
handleApi,
|
|
@@ -41,15 +42,36 @@ const server = createServer(async (req, res) => {
|
|
|
41
42
|
});
|
|
42
43
|
|
|
43
44
|
const requestedPort = boardRuntime.port;
|
|
45
|
+
const requestedHost = process.env.HOST || process.env.ORBIT_HOST || "";
|
|
44
46
|
const hasExplicitPort = Boolean(process.env.PORT);
|
|
45
47
|
const maxAutoPort = requestedPort + 99;
|
|
46
48
|
let currentPort = requestedPort;
|
|
47
49
|
|
|
50
|
+
function listen(port) {
|
|
51
|
+
if (requestedHost) {
|
|
52
|
+
server.listen(port, requestedHost);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
server.listen(port);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function displayHost(host) {
|
|
59
|
+
if (!host || host === "::" || host === "0.0.0.0") return "localhost";
|
|
60
|
+
return host;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function networkUrls(port) {
|
|
64
|
+
return Object.values(networkInterfaces())
|
|
65
|
+
.flat()
|
|
66
|
+
.filter((item) => item && item.family === "IPv4" && !item.internal)
|
|
67
|
+
.map((item) => `http://${item.address}:${port}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
server.on("error", (error) => {
|
|
49
71
|
if (!hasExplicitPort && error?.code === "EADDRINUSE" && currentPort < maxAutoPort) {
|
|
50
72
|
currentPort += 1;
|
|
51
73
|
console.warn(`[orbit] Port ${currentPort - 1} is in use; trying ${currentPort}.`);
|
|
52
|
-
|
|
74
|
+
listen(currentPort);
|
|
53
75
|
return;
|
|
54
76
|
}
|
|
55
77
|
|
|
@@ -58,13 +80,16 @@ server.on("error", (error) => {
|
|
|
58
80
|
process.exit(1);
|
|
59
81
|
});
|
|
60
82
|
|
|
61
|
-
|
|
83
|
+
listen(currentPort);
|
|
62
84
|
|
|
63
85
|
server.on("listening", () => {
|
|
64
86
|
const address = server.address();
|
|
65
87
|
const port = typeof address === "object" && address ? address.port : currentPort;
|
|
66
|
-
const
|
|
88
|
+
const host = typeof address === "object" && address ? displayHost(address.address) : displayHost(requestedHost);
|
|
89
|
+
const summary = startupSummary(port, host);
|
|
67
90
|
console.log(`Starscape Orbit listening on ${summary.url}`);
|
|
91
|
+
const urls = networkUrls(port).filter((url) => url !== summary.url);
|
|
92
|
+
if (urls.length) console.log(`Network: ${urls.join(", ")}`);
|
|
68
93
|
console.log(`Registry: ${summary.registryPath}`);
|
|
69
94
|
console.log(`Boards (${summary.boardCount}):`);
|
|
70
95
|
for (const path of summary.boardPaths) console.log(` - ${path}`);
|