@starlove/orbit 1.1.1 → 1.1.11

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.
Files changed (103) hide show
  1. package/.dockerignore +0 -0
  2. package/Dockerfile +0 -0
  3. package/LICENSE +0 -0
  4. package/README.md +2 -2
  5. package/SKILL-ORBIT.md +2 -0
  6. package/build.mjs +0 -0
  7. package/dist/full/apple-touch-icon.png +0 -0
  8. package/dist/full/cursors/live_arrow.cur +0 -0
  9. package/dist/full/cursors/live_link.cur +0 -0
  10. package/dist/full/cursors/live_move.cur +0 -0
  11. package/dist/full/cursors/live_select.cur +0 -0
  12. package/dist/full/cursors/live_unavail.cur +0 -0
  13. package/dist/full/favicon-32x32.png +0 -0
  14. package/dist/full/index.html +160 -15
  15. package/dist/full/js/app.55b38e12.js +698 -0
  16. package/dist/full/js/app.55b38e12.js.map +7 -0
  17. package/dist/full/orbits-favicon.png +0 -0
  18. package/dist/full/orbits-logo-bg.png +0 -0
  19. package/dist/full/orbits-logo.png +0 -0
  20. package/dist/full/{styles.836872e7.css → styles.273fc37a.css} +79 -39
  21. package/dist/full/version.json +2 -2
  22. package/docs/AGENT_PROTOCOL.md +22 -6
  23. package/docs/MCP_SETUP.md +0 -0
  24. package/docs/ORBIT_DISPATCH.md +68 -8
  25. package/docs/SS-Orbit-Dark.png +0 -0
  26. package/docs/SS-Orbit-Light.png +0 -0
  27. package/docs/mcp-client.example.json +0 -0
  28. package/docs/setup/orbit-init-wizard.md +0 -0
  29. package/package.json +1 -1
  30. package/public/apple-touch-icon.png +0 -0
  31. package/public/cursors/live_arrow.cur +0 -0
  32. package/public/cursors/live_link.cur +0 -0
  33. package/public/cursors/live_move.cur +0 -0
  34. package/public/cursors/live_select.cur +0 -0
  35. package/public/cursors/live_unavail.cur +0 -0
  36. package/public/favicon-32x32.png +0 -0
  37. package/public/index.html +158 -13
  38. package/public/js/actor-labels.js +0 -0
  39. package/public/js/api.js +0 -0
  40. package/public/js/app.js +0 -0
  41. package/public/js/board-menu.js +0 -0
  42. package/public/js/card-actions.js +19 -0
  43. package/public/js/config.js +0 -0
  44. package/public/js/create-card.js +0 -0
  45. package/public/js/dom.js +0 -0
  46. package/public/js/drawer.js +17 -43
  47. package/public/js/format.js +5 -5
  48. package/public/js/kanban.js +157 -92
  49. package/public/js/local-backend.js +15 -4
  50. package/public/js/main.js +2 -1
  51. package/public/js/modal.js +0 -0
  52. package/public/js/motion-preference.js +14 -3
  53. package/public/js/router.js +0 -0
  54. package/public/js/search.js +0 -0
  55. package/public/js/settings-tabs.js +0 -0
  56. package/public/js/settings.js +131 -27
  57. package/public/js/sse.js +0 -0
  58. package/public/js/starscape.js +0 -0
  59. package/public/js/state.js +0 -0
  60. package/public/js/theme-preference.js +0 -0
  61. package/public/js/ticket-detail.js +88 -20
  62. package/public/js/toast.js +0 -0
  63. package/public/js/transport.js +0 -0
  64. package/public/js/unread.js +0 -0
  65. package/public/js/url-routes.js +0 -0
  66. package/public/orbits-favicon.png +0 -0
  67. package/public/orbits-logo-bg.png +0 -0
  68. package/public/orbits-logo.png +0 -0
  69. package/public/styles.css +79 -39
  70. package/src/cli/orbit.js +22 -0
  71. package/src/core/agent.js +25 -4
  72. package/src/core/agents-md.js +0 -0
  73. package/src/core/attachments.js +0 -0
  74. package/src/core/auth.js +0 -0
  75. package/src/core/backups.js +0 -0
  76. package/src/core/board.js +2 -1
  77. package/src/core/boards.js +0 -0
  78. package/src/core/bootstrap.js +0 -0
  79. package/src/core/db.js +19 -0
  80. package/src/core/delete-board.js +0 -0
  81. package/src/core/dispatch.js +178 -13
  82. package/src/core/events.js +0 -0
  83. package/src/core/http.js +0 -0
  84. package/src/core/migrate.js +0 -0
  85. package/src/core/paths.js +0 -0
  86. package/src/core/provision-repo-board.js +0 -0
  87. package/src/core/queries.js +0 -0
  88. package/src/core/registry.js +2 -2
  89. package/src/core/relations.js +0 -0
  90. package/src/core/review-verdicts.js +138 -0
  91. package/src/core/router.js +38 -3
  92. package/src/core/search.js +0 -0
  93. package/src/core/seed.js +0 -0
  94. package/src/core/snapshots.js +429 -9
  95. package/src/core/states.js +0 -0
  96. package/src/core/system-picker.js +0 -0
  97. package/src/core/tickets.js +0 -0
  98. package/src/core/util.js +0 -0
  99. package/src/mcp/orbit-client.js +47 -19
  100. package/src/mcp-server.js +122 -26
  101. package/src/server.js +0 -0
  102. package/dist/full/js/app.a4f045fc.js +0 -676
  103. package/dist/full/js/app.a4f045fc.js.map +0 -7
@@ -1,16 +1,19 @@
1
- import { resetBoard, tx } from "./db.js";
1
+ import { resetBoard, tx, getRegistry } from "./db.js";
2
2
  import { backupBoardDatabase } from "./backups.js";
3
3
  import { boardById, labelsForTicket } from "./queries.js";
4
4
  import { requireBoardAccess } from "./auth.js";
5
5
  import { recordEvent } from "./events.js";
6
- import { syncRegistryFromBoardDb } from "./registry.js";
6
+ import { computeNewBoardDbPath, ensureBoardDbFileAndSchema, syncRegistryFromBoardDb } from "./registry.js";
7
7
  import {
8
8
  httpError,
9
+ id,
9
10
  normalizePriority,
10
11
  normalizeProjectEntryType,
11
12
  normalizeTicketType,
12
13
  now,
13
- requiredString
14
+ normalizePath,
15
+ requiredString,
16
+ slugify
14
17
  } from "./util.js";
15
18
  import { reindexAllTickets } from "./tickets.js";
16
19
  import {
@@ -37,6 +40,11 @@ export function exportBoard(boardId, ctx, options = {}) {
37
40
  .prepare("SELECT * FROM comments WHERE ticket_id IN (SELECT id FROM tickets WHERE board_id = ?) ORDER BY created_at ASC")
38
41
  .all(boardId)
39
42
  : [];
43
+ const reviewVerdicts = ticketIds.length
44
+ ? db
45
+ .prepare("SELECT * FROM review_verdicts WHERE ticket_id IN (SELECT id FROM tickets WHERE board_id = ?) ORDER BY created_at ASC")
46
+ .all(boardId)
47
+ : [];
40
48
  const relations = ticketIds.length
41
49
  ? db
42
50
  .prepare(
@@ -67,7 +75,7 @@ export function exportBoard(boardId, ctx, options = {}) {
67
75
 
68
76
  return {
69
77
  format: "orbit-board-export",
70
- version: 6,
78
+ version: 7,
71
79
  exported_at: now(),
72
80
  include_attachments: Boolean(options.includeAttachments),
73
81
  board: { ...innerBoard },
@@ -75,6 +83,7 @@ export function exportBoard(boardId, ctx, options = {}) {
75
83
  labels,
76
84
  tickets,
77
85
  comments,
86
+ review_verdicts: reviewVerdicts,
78
87
  relations,
79
88
  attachments,
80
89
  board_entries: entries,
@@ -84,7 +93,7 @@ export function exportBoard(boardId, ctx, options = {}) {
84
93
 
85
94
  export function importBoardSnapshot(body, ctx) {
86
95
  const { db, actor, board } = ctx;
87
- const snapshot = body?.snapshot || body;
96
+ const snapshot = normalizeImportSnapshot(body?.snapshot || body, ctx);
88
97
  const format = snapshot?.format;
89
98
  if (!snapshot || format !== "orbit-board-export") throw httpError(400, "invalid_import_snapshot");
90
99
  if (!snapshot.board) throw httpError(400, "snapshot_missing_board");
@@ -160,8 +169,8 @@ export function importBoardSnapshot(body, ctx) {
160
169
  `INSERT INTO tickets
161
170
  (id, board_id, number, title, description, type, parent_ticket_id, ai_plan,
162
171
  implementation_summary, implementation_updates, state_id, priority,
163
- created_by, created_at, updated_at)
164
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
172
+ created_by, created_at, updated_at, archived_at)
173
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
165
174
  ).run(
166
175
  ticket.id,
167
176
  targetBoardId,
@@ -177,7 +186,8 @@ export function importBoardSnapshot(body, ctx) {
177
186
  normalizePriority(ticket.priority),
178
187
  ticket.created_by || "import",
179
188
  ticket.created_at || now(),
180
- ticket.updated_at || now()
189
+ ticket.updated_at || now(),
190
+ ticket.archived_at || null
181
191
  );
182
192
 
183
193
  for (const label of ticket.labels || []) {
@@ -198,6 +208,31 @@ export function importBoardSnapshot(body, ctx) {
198
208
  );
199
209
  }
200
210
 
211
+ for (const review of snapshot.review_verdicts || []) {
212
+ db.prepare(
213
+ `INSERT INTO review_verdicts
214
+ (id, ticket_id, verdict, blocking_findings_json, optional_findings_json,
215
+ evidence_commands_json, reviewer_profile, reviewer_session_id,
216
+ reviewed_commit_sha, dispatch_run_id, supersedes_prior_review_id,
217
+ created_by, created_at)
218
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
219
+ ).run(
220
+ review.id,
221
+ review.ticket_id,
222
+ review.verdict,
223
+ review.blocking_findings_json || "[]",
224
+ review.optional_findings_json || "[]",
225
+ review.evidence_commands_json || "[]",
226
+ review.reviewer_profile || "",
227
+ review.reviewer_session_id || "",
228
+ review.reviewed_commit_sha || "",
229
+ review.dispatch_run_id || "",
230
+ review.supersedes_prior_review_id || null,
231
+ review.created_by || "import",
232
+ review.created_at || now()
233
+ );
234
+ }
235
+
201
236
  for (const relation of snapshot.relations || []) {
202
237
  db.prepare(
203
238
  "INSERT OR IGNORE INTO relations (id, source_ticket_id, target_ticket_id, type, created_at) VALUES (?, ?, ?, ?, ?)"
@@ -256,7 +291,14 @@ export function importBoardSnapshot(body, ctx) {
256
291
  ok: true,
257
292
  imported_board_id: targetBoardId,
258
293
  imported_board_slug: importedBoard.slug,
259
- replace_existing: replaceExisting
294
+ replace_existing: replaceExisting,
295
+ source_format: snapshot.source_format || "orbit-board-export",
296
+ imported_counts: snapshot.imported_counts || {
297
+ states: (snapshot.states || []).length,
298
+ tickets: (snapshot.tickets || []).length,
299
+ comments: (snapshot.comments || []).length,
300
+ labels: (snapshot.labels || []).length
301
+ }
260
302
  };
261
303
  });
262
304
 
@@ -265,3 +307,381 @@ export function importBoardSnapshot(body, ctx) {
265
307
  syncRegistryFromBoardDb(board, db);
266
308
  return result;
267
309
  }
310
+
311
+ export function importBoardSnapshotAsNewBoard(body, ctx) {
312
+ const rawSnapshot = body?.snapshot || body;
313
+ const actor = ctx.actor;
314
+ const sourceBoard = ctx.board || {};
315
+ const repoPath = normalizePath(body?.repo_path || body?.system_path || sourceBoard.repo_path || sourceBoard.system_path || process.cwd());
316
+ const sourceFormatSnapshot = normalizeImportSnapshot(rawSnapshot, {
317
+ ...ctx,
318
+ board: {
319
+ id: id(),
320
+ slug: "",
321
+ name: "",
322
+ repo_path: repoPath,
323
+ system_path: repoPath,
324
+ repo_url: sourceBoard.repo_url || "",
325
+ default_branch: sourceBoard.default_branch || "main"
326
+ }
327
+ });
328
+ const format = sourceFormatSnapshot?.format;
329
+ if (!sourceFormatSnapshot || format !== "orbit-board-export") throw httpError(400, "invalid_import_snapshot");
330
+ if (!sourceFormatSnapshot.board) throw httpError(400, "snapshot_missing_board");
331
+
332
+ const boardName = cleanText(sourceFormatSnapshot.board.name) || cleanText(rawSnapshot?.name) || "Imported Board";
333
+ const boardSlug = uniqueImportedBoardSlug(body?.slug || sourceFormatSnapshot.board.slug || rawSnapshot?.name || boardName);
334
+ const boardId = id();
335
+ const dbPath = computeNewBoardDbPath(repoPath, boardSlug);
336
+ const db = ensureBoardDbFileAndSchema(dbPath);
337
+ const newBoardCtx = {
338
+ actor,
339
+ board: {
340
+ id: boardId,
341
+ slug: boardSlug,
342
+ name: boardName,
343
+ repo_path: repoPath,
344
+ db_path: dbPath,
345
+ system_path: repoPath,
346
+ repo_url: sourceBoard.repo_url || sourceFormatSnapshot.board.repo_url || "",
347
+ default_branch: sourceBoard.default_branch || sourceFormatSnapshot.board.default_branch || "main"
348
+ },
349
+ db
350
+ };
351
+ const normalizedSnapshot = normalizeImportSnapshot(rawSnapshot, newBoardCtx);
352
+ const snapshotForNewBoard = {
353
+ ...normalizedSnapshot,
354
+ board: {
355
+ ...normalizedSnapshot.board,
356
+ id: boardId,
357
+ slug: boardSlug,
358
+ name: cleanText(normalizedSnapshot.board?.name) || boardName,
359
+ repo_url: normalizedSnapshot.board?.repo_url || sourceBoard.repo_url || "",
360
+ system_path: normalizedSnapshot.board?.system_path || repoPath,
361
+ default_branch: normalizedSnapshot.board?.default_branch || sourceBoard.default_branch || "main"
362
+ }
363
+ };
364
+
365
+ return {
366
+ ...importBoardSnapshot({ snapshot: snapshotForNewBoard, replace_existing: false }, newBoardCtx),
367
+ created_new_board: true
368
+ };
369
+ }
370
+
371
+ function uniqueImportedBoardSlug(value) {
372
+ const base = slugify(value) || `imported-board-${id().slice(0, 8)}`;
373
+ const reg = getRegistry();
374
+ if (!reg.prepare("SELECT id FROM boards WHERE slug = ?").get(base)) return base;
375
+ for (let i = 2; i < 1000; i += 1) {
376
+ const candidate = `${base}-${i}`;
377
+ if (!reg.prepare("SELECT id FROM boards WHERE slug = ?").get(candidate)) return candidate;
378
+ }
379
+ return `${base}-${id().slice(0, 8)}`;
380
+ }
381
+
382
+ function normalizeImportSnapshot(snapshot, ctx) {
383
+ if (snapshot?.format === "orbit-board-export") return snapshot;
384
+ if (looksLikeTrelloExport(snapshot)) return trelloExportToOrbitSnapshot(snapshot, ctx);
385
+ return snapshot;
386
+ }
387
+
388
+ function looksLikeTrelloExport(value) {
389
+ return Boolean(
390
+ value &&
391
+ typeof value === "object" &&
392
+ Array.isArray(value.cards) &&
393
+ Array.isArray(value.lists) &&
394
+ (typeof value.id === "string" || typeof value.name === "string")
395
+ );
396
+ }
397
+
398
+ const TRELLO_LABEL_COLORS = {
399
+ green: "#61bd4f",
400
+ yellow: "#f2d600",
401
+ orange: "#ff9f1a",
402
+ red: "#eb5a46",
403
+ purple: "#c377e0",
404
+ blue: "#0079bf",
405
+ sky: "#00c2e0",
406
+ lime: "#51e898",
407
+ pink: "#ff78cb",
408
+ black: "#344563"
409
+ };
410
+
411
+ function trelloExportToOrbitSnapshot(trello, ctx) {
412
+ const time = now();
413
+ const targetBoard = ctx.board || {};
414
+ const boardName = cleanText(trello.name) || "Imported Trello Board";
415
+ const boardSlug = targetBoard.slug || slugify(boardName) || `trello-${id().slice(0, 8)}`;
416
+ const labelsById = buildTrelloLabelMap(trello, time);
417
+ const checklistsByCard = groupTrelloChecklists(trello);
418
+ const commentsByCard = groupTrelloComments(trello);
419
+ const sortedLists = [...(trello.lists || [])].sort(compareTrelloPosition);
420
+ const states = buildTrelloStates(sortedLists, time);
421
+ const fallbackStateId = states[0]?.id;
422
+ const stateIdsByTrelloListId = new Map(sortedLists.map((list, index) => [list.id, states[index]?.id || fallbackStateId]));
423
+ const sortedCards = [...(trello.cards || [])].sort(compareTrelloPosition);
424
+ const tickets = [];
425
+ const comments = [];
426
+
427
+ sortedCards.forEach((card, index) => {
428
+ const ticketId = trelloScopedId("card", card.id || `${index + 1}`);
429
+ const stateId = stateIdsByTrelloListId.get(card.idList) || fallbackStateId;
430
+ if (!stateId) return;
431
+ const cardUpdatedAt = cleanText(card.dateLastActivity) || time;
432
+ const cardCreatedAt = dateFromTrelloObjectId(card.id) || cardUpdatedAt;
433
+ const cardComments = commentsByCard.get(card.id) || [];
434
+ const ticketLabels = trelloLabelsForCard(card, labelsById);
435
+ const importedDescription = renderTrelloCardDescription(card, {
436
+ listName: listNameForCard(trello.lists, card.idList),
437
+ checklists: checklistsByCard.get(card.id) || []
438
+ });
439
+ tickets.push({
440
+ id: ticketId,
441
+ number: index + 1,
442
+ title: cleanText(card.name) || `Untitled Trello card ${index + 1}`,
443
+ description: importedDescription,
444
+ type: "task",
445
+ parent_ticket_id: null,
446
+ ai_plan: "",
447
+ implementation_summary: "",
448
+ implementation_updates: "",
449
+ state_id: stateId,
450
+ priority: priorityFromTrelloCard(card),
451
+ created_by: "trello-import",
452
+ created_at: cardCreatedAt,
453
+ updated_at: cardUpdatedAt,
454
+ archived_at: card.closed ? cardUpdatedAt : null,
455
+ labels: ticketLabels
456
+ });
457
+
458
+ cardComments.forEach((action, commentIndex) => {
459
+ const commentId = trelloScopedId("comment", action.id || `${card.id}-${commentIndex + 1}`);
460
+ comments.push({
461
+ id: commentId,
462
+ ticket_id: ticketId,
463
+ author: trelloMemberName(action.memberCreator),
464
+ kind: "human_comment",
465
+ body: cleanText(action.data?.text) || "",
466
+ created_at: cleanText(action.date) || cardUpdatedAt
467
+ });
468
+ });
469
+ });
470
+
471
+ return {
472
+ format: "orbit-board-export",
473
+ source_format: "trello-board-export",
474
+ version: 7,
475
+ exported_at: time,
476
+ include_attachments: false,
477
+ imported_counts: {
478
+ states: states.length,
479
+ tickets: tickets.length,
480
+ comments: comments.length,
481
+ labels: labelsById.size
482
+ },
483
+ board: {
484
+ id: targetBoard.id || trelloScopedId("board", trello.id || id()),
485
+ slug: boardSlug,
486
+ name: boardName,
487
+ repo_url: targetBoard.repo_url || "",
488
+ system_path: targetBoard.system_path || targetBoard.repo_path || "",
489
+ default_branch: targetBoard.default_branch || "main",
490
+ project_notes: cleanText(trello.desc) ? `Imported from Trello.\n\n${cleanText(trello.desc)}` : "Imported from Trello.",
491
+ agent_instructions: "",
492
+ ai_enabled: 1,
493
+ created_at: cleanText(trello.dateLastActivity) || time,
494
+ updated_at: time
495
+ },
496
+ states,
497
+ labels: [...labelsById.values()],
498
+ tickets,
499
+ comments: comments.filter((comment) => comment.body),
500
+ review_verdicts: [],
501
+ relations: [],
502
+ attachments: [],
503
+ board_entries: [],
504
+ events: []
505
+ };
506
+ }
507
+
508
+ function buildTrelloStates(lists = [], time) {
509
+ const sorted = [...lists].sort(compareTrelloPosition);
510
+ const rolesSeen = new Set();
511
+ const firstOpenIndex = sorted.findIndex((list) => !list.closed);
512
+ const defaultIndex = firstOpenIndex >= 0 ? firstOpenIndex : 0;
513
+ return sorted.map((list, index) => {
514
+ const role = uniqueTrelloStateRole(inferTrelloStateRole(list.name), rolesSeen);
515
+ return {
516
+ id: trelloScopedId("list", list.id || `${index + 1}`),
517
+ name: cleanText(list.name) || `Trello List ${index + 1}`,
518
+ position: index,
519
+ is_default: index === defaultIndex ? 1 : 0,
520
+ role,
521
+ created_at: dateFromTrelloObjectId(list.id) || time
522
+ };
523
+ });
524
+ }
525
+
526
+ function buildTrelloLabelMap(trello, time) {
527
+ const labels = new Map();
528
+ const addLabel = (raw) => {
529
+ if (!raw) return;
530
+ const key = raw.id || `${raw.color || "label"}-${raw.name || ""}`;
531
+ const name = trelloLabelName(raw, trello.labelNames);
532
+ if (!name || labels.has(key)) return;
533
+ labels.set(key, {
534
+ id: trelloScopedId("label", key),
535
+ name,
536
+ color: TRELLO_LABEL_COLORS[String(raw.color || "").toLowerCase()] || "#64748b",
537
+ created_at: dateFromTrelloObjectId(raw.id) || time
538
+ });
539
+ };
540
+ for (const label of trello.labels || []) addLabel(label);
541
+ for (const card of trello.cards || []) for (const label of card.labels || []) addLabel(label);
542
+ return labels;
543
+ }
544
+
545
+ function groupTrelloChecklists(trello) {
546
+ const grouped = new Map();
547
+ const add = (cardId, checklist) => {
548
+ if (!cardId || !checklist) return;
549
+ if (!grouped.has(cardId)) grouped.set(cardId, []);
550
+ grouped.get(cardId).push(checklist);
551
+ };
552
+ for (const checklist of trello.checklists || []) add(checklist.idCard, checklist);
553
+ for (const card of trello.cards || []) for (const checklist of card.checklists || []) add(card.id, checklist);
554
+ return grouped;
555
+ }
556
+
557
+ function groupTrelloComments(trello) {
558
+ const grouped = new Map();
559
+ for (const action of trello.actions || []) {
560
+ if (action.type !== "commentCard") continue;
561
+ const cardId = action.data?.card?.id;
562
+ if (!cardId) continue;
563
+ if (!grouped.has(cardId)) grouped.set(cardId, []);
564
+ grouped.get(cardId).push(action);
565
+ }
566
+ for (const actions of grouped.values()) actions.sort((a, b) => String(a.date || "").localeCompare(String(b.date || "")));
567
+ return grouped;
568
+ }
569
+
570
+ function trelloLabelsForCard(card, labelsById) {
571
+ const result = [];
572
+ const seen = new Set();
573
+ const addByRawId = (rawId) => {
574
+ const label = labelsById.get(rawId);
575
+ if (label && !seen.has(label.id)) {
576
+ seen.add(label.id);
577
+ result.push(label);
578
+ }
579
+ };
580
+ for (const label of card.labels || []) addByRawId(label.id || `${label.color || "label"}-${label.name || ""}`);
581
+ for (const labelId of card.idLabels || []) addByRawId(labelId);
582
+ return result;
583
+ }
584
+
585
+ function renderTrelloCardDescription(card, { listName, checklists }) {
586
+ const parts = [];
587
+ const desc = cleanText(card.desc);
588
+ if (desc) parts.push(desc);
589
+ const meta = [];
590
+ if (card.idShort) meta.push(`Trello card #${card.idShort}`);
591
+ if (listName) meta.push(`List: ${listName}`);
592
+ if (card.shortUrl || card.url) meta.push(`URL: ${card.shortUrl || card.url}`);
593
+ if (card.due) meta.push(`Due: ${card.due}${card.dueComplete ? " (complete)" : ""}`);
594
+ if (card.closed) meta.push("Archived in Trello: yes");
595
+ if (meta.length) parts.push(["Imported from Trello:", ...meta.map((line) => `- ${line}`)].join("\n"));
596
+
597
+ const checklistText = renderTrelloChecklists(checklists);
598
+ if (checklistText) parts.push(checklistText);
599
+ const attachmentText = renderTrelloAttachments(card.attachments || []);
600
+ if (attachmentText) parts.push(attachmentText);
601
+ return parts.join("\n\n");
602
+ }
603
+
604
+ function renderTrelloChecklists(checklists = []) {
605
+ const rendered = checklists
606
+ .map((checklist) => {
607
+ const items = [...(checklist.checkItems || [])].sort(compareTrelloPosition);
608
+ if (!items.length) return "";
609
+ return [`Checklist: ${cleanText(checklist.name) || "Untitled"}`, ...items.map((item) => `- [${item.state === "complete" ? "x" : " "}] ${cleanText(item.name) || "Untitled item"}`)].join("\n");
610
+ })
611
+ .filter(Boolean);
612
+ return rendered.join("\n\n");
613
+ }
614
+
615
+ function renderTrelloAttachments(attachments = []) {
616
+ const rendered = attachments
617
+ .map((attachment) => {
618
+ const name = cleanText(attachment.name) || cleanText(attachment.fileName) || "Attachment";
619
+ const url = cleanText(attachment.url);
620
+ return url ? `- ${name}: ${url}` : `- ${name}`;
621
+ })
622
+ .filter(Boolean);
623
+ return rendered.length ? ["Trello attachments:", ...rendered].join("\n") : "";
624
+ }
625
+
626
+ function trelloLabelName(label, labelNames = {}) {
627
+ const explicit = cleanText(label.name);
628
+ if (explicit) return explicit;
629
+ const color = cleanText(label.color);
630
+ const namedColor = color ? cleanText(labelNames[color]) : "";
631
+ if (namedColor) return namedColor;
632
+ return color ? `Trello ${color}` : "";
633
+ }
634
+
635
+ function listNameForCard(lists = [], idList) {
636
+ return cleanText(lists.find((list) => list.id === idList)?.name);
637
+ }
638
+
639
+ function priorityFromTrelloCard(card) {
640
+ const names = (card.labels || []).map((label) => cleanText(label.name || label.color).toLowerCase()).join(" ");
641
+ if (/urgent|critical|blocker|high|red/.test(names)) return 4;
642
+ if (/low|someday|backlog/.test(names)) return 1;
643
+ return 2;
644
+ }
645
+
646
+ function inferTrelloStateRole(name) {
647
+ const normalized = cleanText(name).toLowerCase();
648
+ if (!normalized) return null;
649
+ if (/^ai ready$|ai[- ]ready/.test(normalized)) return "ai_ready";
650
+ if (/review|qa|verify|validation/.test(normalized)) return "review";
651
+ if (/doing|in progress|active|working|wip/.test(normalized)) return "in_progress";
652
+ if (/done|complete|completed|shipped|closed/.test(normalized)) return "done";
653
+ return null;
654
+ }
655
+
656
+ function uniqueTrelloStateRole(role, rolesSeen) {
657
+ if (!role || rolesSeen.has(role)) return null;
658
+ rolesSeen.add(role);
659
+ return role;
660
+ }
661
+
662
+ function trelloMemberName(member) {
663
+ return cleanText(member?.fullName) || cleanText(member?.username) || "Trello";
664
+ }
665
+
666
+ function compareTrelloPosition(a, b) {
667
+ const pa = Number(a?.pos ?? Number.MAX_SAFE_INTEGER);
668
+ const pb = Number(b?.pos ?? Number.MAX_SAFE_INTEGER);
669
+ if (pa !== pb) return pa - pb;
670
+ return String(a?.id || "").localeCompare(String(b?.id || ""));
671
+ }
672
+
673
+ function dateFromTrelloObjectId(value) {
674
+ const idValue = String(value || "");
675
+ if (!/^[0-9a-fA-F]{8}/.test(idValue)) return null;
676
+ const seconds = Number.parseInt(idValue.slice(0, 8), 16);
677
+ if (!Number.isFinite(seconds)) return null;
678
+ return new Date(seconds * 1000).toISOString();
679
+ }
680
+
681
+ function cleanText(value) {
682
+ return String(value ?? "").replace(/\r\n/g, "\n").trim();
683
+ }
684
+
685
+ function trelloScopedId(prefix, value) {
686
+ return `trello-${prefix}-${String(value || id()).replace(/[^a-zA-Z0-9_-]/g, "-")}`;
687
+ }
File without changes
File without changes
File without changes
package/src/core/util.js CHANGED
File without changes
@@ -20,7 +20,7 @@ export async function createOrbitClient(env = process.env) {
20
20
 
21
21
  export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.fetch) {
22
22
  const apiUrl = String(env.ORBIT_API_URL || "").trim().replace(/\/+$/, "");
23
- const defaultBoard = String(env.ORBIT_DEFAULT_BOARD || "").trim();
23
+ let defaultBoard = String(env.ORBIT_DEFAULT_BOARD || "").trim();
24
24
  if (!apiUrl) throw rpcError(-32602, "ORBIT_MODE=remote requires ORBIT_API_URL.");
25
25
  if (typeof fetchImpl !== "function") throw rpcError(-32603, "ORBIT_MODE=remote requires global fetch support.");
26
26
 
@@ -57,6 +57,10 @@ 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 stripBoardSelectors = (args = {}) => {
61
+ const { board_id, board_slug, board, ...rest } = args;
62
+ return rest;
63
+ };
60
64
  const searchQuery = (args = {}) => ({
61
65
  q: args.q,
62
66
  limit: args.limit,
@@ -84,9 +88,10 @@ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.
84
88
  return request("GET", `/api/boards/${encodeURIComponent(requireBoard(args))}/context`, { query: { include_struck: args.include_struck ? "true" : undefined } });
85
89
  },
86
90
  async boardList() { return request("GET", "/api/boards"); },
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" }; },
91
+ async boardSetActive(args = {}) { if (!args.slug) throw rpcError(-32602, "slug is required."); defaultBoard = String(args.slug); env.ORBIT_DEFAULT_BOARD = defaultBoard; return { ok: true, slug: defaultBoard, mode: "remote" }; },
88
92
  async claimNext(args = {}) { return request("POST", "/api/agent/claim-next", { body: { ...args, board: requireBoard(args) } }); },
89
93
  async getTicketContext(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/context`, { query: { ...contextQuery(args), depth: args.depth || 1 } }); },
94
+ async getTicketContextFull(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/context/full`, { query: { ...contextQuery(args), depth: args.depth || 1 } }); },
90
95
  async getAgentDispatchPacket(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/dispatch-packet`, { query: contextQuery(args) }); },
91
96
  async readTicket(args = {}) {
92
97
  if (args.ticket_id) {
@@ -113,11 +118,14 @@ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.
113
118
  async getTicketBlockers(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/blockers`, { query: ticketQuery(args) }); },
114
119
  async search(args = {}) { return request("GET", "/api/search", { query: searchQuery(args) }); },
115
120
  async createTicket(args = {}) { return request("POST", "/api/tickets", { body: { ...args, board: requireBoard(args) }, ok: [201] }); },
116
- async updateTicket({ ticket_id, ...patch }) { return request("PATCH", `/api/tickets/${encodeURIComponent(ticket_id)}`, { query: ticketQuery(patch), body: patch }); },
117
- async addComment({ ticket_id, ...body }) { return request("POST", `/api/tickets/${encodeURIComponent(ticket_id)}/comments`, { body, ok: [201] }); },
118
- async addBoardEntry(args = {}) { return request("POST", `/api/boards/${encodeURIComponent(requireBoard(args))}/entries`, { body: args, ok: [201] }); },
119
- async checkpoint(args = {}) { return request("POST", "/api/agent/checkpoint", { body: args }); },
120
- async complete(args = {}) { return request("POST", "/api/agent/complete", { body: args }); },
121
+ async updateTicket({ ticket_id, ...patch }) { return request("PATCH", `/api/tickets/${encodeURIComponent(ticket_id)}`, { query: ticketQuery(patch), body: stripBoardSelectors(patch) }); },
122
+ async addComment({ ticket_id, ...body }) { return request("POST", `/api/tickets/${encodeURIComponent(ticket_id)}/comments`, { query: ticketQuery(body), body: stripBoardSelectors(body), ok: [201] }); },
123
+ async createReviewVerdict({ ticket_id, ...body }) { return request("POST", `/api/tickets/${encodeURIComponent(ticket_id)}/review-verdicts`, { query: ticketQuery(body), body: stripBoardSelectors(body), ok: [201] }); },
124
+ async listReviewVerdicts(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/review-verdicts`, { query: { ...ticketQuery(args), limit: args.limit } }); },
125
+ async getReviewVerdict(args = {}) { return request("GET", `/api/review-verdicts/${encodeURIComponent(args.review_id)}`, { query: ticketQuery(args) }); },
126
+ async addBoardEntry(args = {}) { return request("POST", `/api/boards/${encodeURIComponent(requireBoard(args))}/entries`, { body: stripBoardSelectors(args), ok: [201] }); },
127
+ async checkpoint(args = {}) { const fallback = boardArg(args); return request("POST", "/api/agent/checkpoint", { body: fallback ? { ...args, board: args.board || args.board_slug || fallback } : args }); },
128
+ async complete(args = {}) { const fallback = boardArg(args); return request("POST", "/api/agent/complete", { body: fallback ? { ...args, board: args.board || args.board_slug || fallback } : args }); },
121
129
  async archiveTicket(args = {}) { return request("POST", `/api/tickets/${encodeURIComponent(args.ticket_id)}/archive`, { query: ticketQuery(args) }); },
122
130
  async restoreTicket(args = {}) { return request("POST", `/api/tickets/${encodeURIComponent(args.ticket_id)}/restore`, { query: ticketQuery(args) }); },
123
131
  async deleteTicket(args = {}) { return request("DELETE", `/api/tickets/${encodeURIComponent(args.ticket_id)}`, { query: ticketQuery(args) }); },
@@ -198,10 +206,26 @@ export async function createLocalOrbitClient(env = process.env) {
198
206
  if (!row) throw rpcError(-32602, "No board in session; pass board_id or board_slug, or call board_set_active first.");
199
207
  return { actor, board: row, db: registry.openBoardDb(row) };
200
208
  };
201
- const sessionCtx = (actor) => ctxFor(getSessionBoard(), actor);
202
209
  const backup = (ctx, result) => { backups.scheduleAutomaticBoardBackup(ctx.board, ctx.db); return result; };
203
- const explicit = (args = {}) => args.board_id ? registry.getBoardByRegistryId(args.board_id) : (args.board_slug || args.board ? registry.getBoardBySlug(args.board_slug || args.board) : null);
210
+ const explicit = (args = {}) => {
211
+ if (args.board_id) {
212
+ const row = registry.getBoardByRegistryId(args.board_id);
213
+ if (!row) throw rpcError(-32004, `Board id not found: ${args.board_id}`);
214
+ return row;
215
+ }
216
+ if (args.board_slug || args.board) {
217
+ const slug = args.board_slug || args.board;
218
+ const row = registry.getBoardBySlug(slug);
219
+ if (!row) throw rpcError(-32004, `Board slug not found: ${slug}`);
220
+ return row;
221
+ }
222
+ return null;
223
+ };
204
224
  const rowOrSession = (args = {}) => explicit(args) || getSessionBoard();
225
+ const stripBoardSelectors = (args = {}) => {
226
+ const { board_id, board_slug, board: _board, ...rest } = args;
227
+ return rest;
228
+ };
205
229
  const actor = () => board.localAgentActor();
206
230
 
207
231
  return {
@@ -216,21 +240,25 @@ export async function createLocalOrbitClient(env = process.env) {
216
240
  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 }; },
217
241
  claimNext: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.claimNext(args, ctx)); },
218
242
  getTicketContext: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getContextPack(args.ticket_id, ctx, Number(args.depth || 1), args); },
243
+ getTicketContextFull: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getContextPackFull(args.ticket_id, ctx, Number(args.depth || 1), args); },
219
244
  getAgentDispatchPacket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getAgentDispatchPacket(args.ticket_id, ctx, args); },
220
245
  readTicket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.readTicket(args, ctx); },
221
246
  readComments: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.readComments(args, ctx); },
222
247
  getTicketRelations: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getTicketRelations(args.ticket_id, ctx); },
223
248
  getTicketBlockers: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getTicketBlockers(args.ticket_id, ctx); },
224
249
  search: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.searchTickets(args, ctx); },
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)); },
226
- updateTicket: ({ ticket_id, ...patch }) => { const ctx = sessionCtx(actor()); return backup(ctx, board.updateTicket(ticket_id, patch, ctx)); },
227
- addComment: ({ ticket_id, ...body }) => { const ctx = sessionCtx(actor()); return backup(ctx, board.createComment(ticket_id, body, ctx)); },
228
- addBoardEntry: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.createBoardEntry(ctx.board.id, args, ctx)); },
229
- checkpoint: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.checkpointTicket(args, ctx)); },
230
- complete: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.completeTicket(args, ctx)); },
231
- archiveTicket: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.archiveTicket(args.ticket_id, ctx)); },
232
- restoreTicket: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.restoreTicket(args.ticket_id, ctx)); },
233
- deleteTicket: (args = {}) => { const ctx = sessionCtx(actor()); return backup(ctx, board.deleteTicket(args.ticket_id, ctx)); },
250
+ 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
+ updateTicket: ({ ticket_id, ...patch }) => { const ctx = ctxFor(rowOrSession(patch), actor()); return backup(ctx, board.updateTicket(ticket_id, stripBoardSelectors(patch), ctx)); },
252
+ addComment: ({ ticket_id, ...body }) => { const ctx = ctxFor(rowOrSession(body), actor()); return backup(ctx, board.createComment(ticket_id, stripBoardSelectors(body), ctx)); },
253
+ createReviewVerdict: ({ ticket_id, ...body }) => { const ctx = ctxFor(rowOrSession(body), actor()); return backup(ctx, board.createReviewVerdict(ticket_id, stripBoardSelectors(body), ctx)); },
254
+ listReviewVerdicts: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return { review_verdicts: board.listReviewVerdicts(args.ticket_id, ctx, args) }; },
255
+ getReviewVerdict: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getReviewVerdict(args.review_id, ctx); },
256
+ addBoardEntry: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.createBoardEntry(ctx.board.id, stripBoardSelectors(args), ctx)); },
257
+ checkpoint: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.checkpointTicket(stripBoardSelectors(args), ctx)); },
258
+ complete: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.completeTicket(stripBoardSelectors(args), ctx)); },
259
+ archiveTicket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.archiveTicket(args.ticket_id, ctx)); },
260
+ restoreTicket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.restoreTicket(args.ticket_id, ctx)); },
261
+ deleteTicket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.deleteTicket(args.ticket_id, ctx)); },
234
262
  listArchive: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return { tickets: board.archivedTicketsForBoard(ctx.db, ctx.board.id) }; },
235
263
  exportBoard: (args = {}) => {
236
264
  const ctx = ctxFor(rowOrSession(args), actor());
@@ -238,6 +266,6 @@ export async function createLocalOrbitClient(env = process.env) {
238
266
  includeAttachments: Boolean(args.include_attachments || args.include_images)
239
267
  });
240
268
  },
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)); }
269
+ updateSettings: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); const patch = stripBoardSelectors(args); return backup(ctx, board.updateBoard(ctx.board.id, patch, ctx)); }
242
270
  };
243
271
  }