@launchsecure/launch-kit 0.0.40 → 0.0.42

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 (106) hide show
  1. package/dist/chart-client/assets/index-Dd6IotOZ.css +1 -0
  2. package/dist/chart-client/index.html +2 -2
  3. package/dist/client/assets/index-DE0uje6k.css +32 -0
  4. package/dist/client/index.html +2 -2
  5. package/dist/council-client/assets/index-CGYusOCK.css +1 -0
  6. package/dist/council-client/assets/{index-B1v46vTE.js → index-DkTFX53U.js} +1 -1
  7. package/dist/council-client/index.html +2 -2
  8. package/dist/deck-client/assets/_baseUniq-BrhDuG3C.js +1 -0
  9. package/dist/deck-client/assets/arc-DXtPHMhw.js +1 -0
  10. package/dist/deck-client/assets/architectureDiagram-Q4EWVU46-Cs9IdLQQ.js +36 -0
  11. package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-CUdblaWk.js → blockDiagram-DXYQGD6D--wpvoBAt.js} +2 -2
  12. package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-MfAO5lak.js → c4Diagram-AHTNJAMY-BNIKE8-Z.js} +2 -2
  13. package/dist/deck-client/assets/channel-Bb5wIjTD.js +1 -0
  14. package/dist/deck-client/assets/chunk-4BX2VUAB-Bz3EWmUo.js +1 -0
  15. package/dist/deck-client/assets/{chunk-4TB4RGXK-BUJtZ7jO.js → chunk-4TB4RGXK-D55BBvVZ.js} +1 -1
  16. package/dist/deck-client/assets/chunk-55IACEB6-BzjEcoTi.js +1 -0
  17. package/dist/deck-client/assets/chunk-EDXVE4YY-CH1vs4Lu.js +1 -0
  18. package/dist/deck-client/assets/{chunk-FMBD7UC4-PnZ9v6ey.js → chunk-FMBD7UC4-CVRnaGM0.js} +1 -1
  19. package/dist/deck-client/assets/{chunk-OYMX7WX6-DXrWNOsV.js → chunk-OYMX7WX6-BCTV_aEZ.js} +1 -1
  20. package/dist/deck-client/assets/chunk-QZHKN3VN-BqM8r4ee.js +1 -0
  21. package/dist/deck-client/assets/chunk-YZCP3GAM-Rhbb691A.js +1 -0
  22. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-CN-ZYMbT.js +1 -0
  23. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-CN-ZYMbT.js +1 -0
  24. package/dist/deck-client/assets/clone-D6e7poKG.js +1 -0
  25. package/dist/deck-client/assets/cose-bilkent-S5V4N54A-fWyBR9Nn.js +1 -0
  26. package/dist/deck-client/assets/dagre-KV5264BT-Ds0Sqext.js +4 -0
  27. package/dist/deck-client/assets/diagram-5BDNPKRD-BGz_X007.js +10 -0
  28. package/dist/deck-client/assets/diagram-G4DWMVQ6-C78jeb-r.js +24 -0
  29. package/dist/deck-client/assets/diagram-MMDJMWI5-BdoHW8mF.js +43 -0
  30. package/dist/deck-client/assets/diagram-TYMM5635-C8i9zc-U.js +24 -0
  31. package/dist/deck-client/assets/{erDiagram-SMLLAGMA-56pn_93p.js → erDiagram-SMLLAGMA-DplAZ-yG.js} +2 -2
  32. package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-BtV3M5xJ.js → flowDiagram-DWJPFMVM-F3N9yTAB.js} +2 -2
  33. package/dist/deck-client/assets/ganttDiagram-T4ZO3ILL-DebdM4S5.js +292 -0
  34. package/dist/deck-client/assets/gitGraphDiagram-UUTBAWPF-htCk0oPz.js +106 -0
  35. package/dist/deck-client/assets/graph-Dtxc2PT4.js +1 -0
  36. package/dist/deck-client/assets/index-CwAiam97.js +892 -0
  37. package/dist/deck-client/assets/index-evAPhGvM.css +1 -0
  38. package/dist/deck-client/assets/infoDiagram-42DDH7IO-gTxegviJ.js +2 -0
  39. package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DXYkdO3T.js → ishikawaDiagram-UXIWVN3A-DYSEsfUK.js} +5 -5
  40. package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-C2zBr-J5.js → journeyDiagram-VCZTEJTY-DgVc1q4D.js} +2 -2
  41. package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-CdoYLS4Z.js → kanban-definition-6JOO6SKY-DcOf3i9N.js} +2 -2
  42. package/dist/deck-client/assets/layout-CHP9HIw4.js +1 -0
  43. package/dist/deck-client/assets/linear-XjVb7x4Q.js +1 -0
  44. package/dist/deck-client/assets/mermaid.core-bouKOnsR.js +309 -0
  45. package/dist/deck-client/assets/min-Jl4GV9DB.js +1 -0
  46. package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-oAybLedr.js → mindmap-definition-QFDTVHPH-FXXf4cJx.js} +2 -2
  47. package/dist/deck-client/assets/pieDiagram-DEJITSTG-Dl4plCTi.js +30 -0
  48. package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-dtluDZXs.js → quadrantDiagram-34T5L4WZ-BeOEeelg.js} +2 -2
  49. package/dist/deck-client/assets/{requirementDiagram-MS252O5E-Cq8l7bOl.js → requirementDiagram-MS252O5E-CBh1jchM.js} +2 -2
  50. package/dist/deck-client/assets/sankeyDiagram-XADWPNL6-BK6gG-ub.js +10 -0
  51. package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-CYkd7oQK.js → sequenceDiagram-FGHM5R23-BtQwzsF5.js} +2 -2
  52. package/dist/deck-client/assets/stateDiagram-FHFEXIEX--kLQOi8R.js +1 -0
  53. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CHEPjgB0.js +1 -0
  54. package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-DZIxSyd1.js → timeline-definition-GMOUNBTQ-CQ_-CLh7.js} +2 -2
  55. package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-Ct4JVRDM.js → vennDiagram-DHZGUBPP-DBhKYAFT.js} +2 -2
  56. package/dist/deck-client/assets/{wardley-RL74JXVD-V29ycxOW.js → wardley-RL74JXVD-DLKe29q9.js} +3 -3
  57. package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-D-Ua6Cmi.js → wardleyDiagram-NUSXRM2D-Brb8ezsV.js} +2 -2
  58. package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-BPCOuRVl.js → xychartDiagram-5P7HB3ND-LogZ0Az9.js} +2 -2
  59. package/dist/deck-client/index.html +2 -2
  60. package/dist/server/cli.js +488 -215
  61. package/dist/server/council-entry.js +7 -1
  62. package/dist/server/council-serve.js +7 -1
  63. package/dist/server/deck-mcp-entry.js +423 -50
  64. package/dist/server/deck-serve.js +350 -48
  65. package/dist/server/graph-mcp-entry.js +331 -37
  66. package/dist/server/init-entry.js +17 -7
  67. package/dist/server/rover-entry.js +1 -1
  68. package/package.json +1 -1
  69. package/scaffolds/ls-marketplace/plugins/kit/skills/comms/SKILL.md +34 -1
  70. package/scaffolds/ls-marketplace/plugins/kit/skills/gen-test/SKILL.md +126 -0
  71. package/dist/chart-client/assets/index-CWJFFDPu.css +0 -1
  72. package/dist/client/assets/index-CTzFcfGV.css +0 -32
  73. package/dist/council-client/assets/index-ArgRc5mN.css +0 -1
  74. package/dist/deck-client/assets/_baseUniq-BZP7n41F.js +0 -1
  75. package/dist/deck-client/assets/arc-31biU3Az.js +0 -1
  76. package/dist/deck-client/assets/architectureDiagram-Q4EWVU46-DHg6Ss--.js +0 -36
  77. package/dist/deck-client/assets/channel-BBkRLdnC.js +0 -1
  78. package/dist/deck-client/assets/chunk-4BX2VUAB-DQ1MrGgN.js +0 -1
  79. package/dist/deck-client/assets/chunk-55IACEB6-BdSnXB6g.js +0 -1
  80. package/dist/deck-client/assets/chunk-EDXVE4YY-94yZIUI8.js +0 -1
  81. package/dist/deck-client/assets/chunk-QZHKN3VN-CsIGIDKX.js +0 -1
  82. package/dist/deck-client/assets/chunk-YZCP3GAM-DVkBO9tn.js +0 -1
  83. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-DFCaeF-7.js +0 -1
  84. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-DFCaeF-7.js +0 -1
  85. package/dist/deck-client/assets/clone-GCEVRScB.js +0 -1
  86. package/dist/deck-client/assets/cose-bilkent-S5V4N54A-m126Oh3b.js +0 -1
  87. package/dist/deck-client/assets/dagre-KV5264BT-C2aig8U5.js +0 -4
  88. package/dist/deck-client/assets/diagram-5BDNPKRD-CKpoRfGn.js +0 -10
  89. package/dist/deck-client/assets/diagram-G4DWMVQ6-Cjh115Ep.js +0 -24
  90. package/dist/deck-client/assets/diagram-MMDJMWI5-DKlBv_2L.js +0 -43
  91. package/dist/deck-client/assets/diagram-TYMM5635-CdBh4cEn.js +0 -24
  92. package/dist/deck-client/assets/ganttDiagram-T4ZO3ILL-DTIsC6Zg.js +0 -292
  93. package/dist/deck-client/assets/gitGraphDiagram-UUTBAWPF-CJYeyCLe.js +0 -106
  94. package/dist/deck-client/assets/graph-BDvMu1Ss.js +0 -1
  95. package/dist/deck-client/assets/index-D4eSxcBn.css +0 -1
  96. package/dist/deck-client/assets/index-QnGVE9PZ.js +0 -1196
  97. package/dist/deck-client/assets/infoDiagram-42DDH7IO-BWyKJnpW.js +0 -2
  98. package/dist/deck-client/assets/layout-vOnxnCQU.js +0 -1
  99. package/dist/deck-client/assets/linear-B0J0WCGz.js +0 -1
  100. package/dist/deck-client/assets/min-B0AXlT9L.js +0 -1
  101. package/dist/deck-client/assets/pieDiagram-DEJITSTG-BjHyHxGk.js +0 -30
  102. package/dist/deck-client/assets/sankeyDiagram-XADWPNL6-C1Vih91z.js +0 -10
  103. package/dist/deck-client/assets/stateDiagram-FHFEXIEX-CtyG8wBK.js +0 -1
  104. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BLyKWfcN.js +0 -1
  105. /package/dist/chart-client/assets/{index-Dzlj-oCj.js → index-CrYM1-ac.js} +0 -0
  106. /package/dist/client/assets/{index-tTg_ezUF.js → index-BoIjawzY.js} +0 -0
@@ -1421,7 +1421,13 @@ async function callTool(toolName, args) {
1421
1421
  }
1422
1422
  const textContent = result.result?.content?.[0]?.text;
1423
1423
  if (!textContent) return null;
1424
- return JSON.parse(textContent);
1424
+ try {
1425
+ return JSON.parse(textContent);
1426
+ } catch {
1427
+ const match = textContent.match(/──+\s*Error\s*──+\s*\n([\s\S]*?)(?:\n\n──|$)/i);
1428
+ const message = (match?.[1] ?? textContent).trim();
1429
+ throw new Error(message || "MCP tool returned an unparseable response");
1430
+ }
1425
1431
  }
1426
1432
  async function writeDeck(input) {
1427
1433
  const result = await callTool("communication_write", {
@@ -1430,6 +1436,7 @@ async function writeDeck(input) {
1430
1436
  resource_type: "deck",
1431
1437
  fields: {
1432
1438
  deckHtml: input.html,
1439
+ deckBlocks: input.blocks,
1433
1440
  sessionId: input.sessionId,
1434
1441
  blockCount: input.blockCount,
1435
1442
  sharedFrom: "launch-deck"
@@ -1444,6 +1451,7 @@ async function updateDeck(commentId, input) {
1444
1451
  body: input.body,
1445
1452
  fields: {
1446
1453
  deckHtml: input.html,
1454
+ deckBlocks: input.blocks,
1447
1455
  sessionId: input.sessionId,
1448
1456
  blockCount: input.blockCount,
1449
1457
  sharedFrom: "launch-deck"
@@ -1454,6 +1462,49 @@ async function updateDeck(commentId, input) {
1454
1462
  async function deleteDeck(commentId) {
1455
1463
  await callTool("communication_delete", { comment_id: commentId });
1456
1464
  }
1465
+ function authorLabel(c) {
1466
+ return c.author?.name || c.author?.email || "unknown";
1467
+ }
1468
+ async function readCloudDeck(commentId) {
1469
+ const result = await callTool("communication_read", {
1470
+ resource_type: "deck",
1471
+ limit: 50
1472
+ });
1473
+ const deck = result?.comments?.find((c) => c.id === commentId);
1474
+ if (!deck) return null;
1475
+ return {
1476
+ id: deck.id,
1477
+ title: deck.title ?? "(untitled deck)",
1478
+ author: authorLabel(deck),
1479
+ html: typeof deck.fields?.deckHtml === "string" ? deck.fields.deckHtml : null,
1480
+ blocks: Array.isArray(deck.fields?.deckBlocks) && deck.fields.deckBlocks.length > 0 ? deck.fields.deckBlocks : null
1481
+ };
1482
+ }
1483
+ function currentMcpOrigin() {
1484
+ const config = _config;
1485
+ if (!config) throw new Error("MCP config not loaded \u2014 call loadMcpConfig first");
1486
+ return new URL(config.url).origin;
1487
+ }
1488
+ async function createDeckShareLink(commentId) {
1489
+ const result = await callTool("deck_share_link", {
1490
+ comment_id: commentId,
1491
+ action: "create"
1492
+ });
1493
+ const token = result.link.token;
1494
+ return {
1495
+ linkId: result.link.id,
1496
+ token,
1497
+ shareUrl: `${currentMcpOrigin()}/s/deck/${token}`,
1498
+ reused: Boolean(result.reused)
1499
+ };
1500
+ }
1501
+ async function revokeDeckShareLink(commentId, linkId) {
1502
+ await callTool("deck_share_link", {
1503
+ comment_id: commentId,
1504
+ action: "revoke",
1505
+ link_id: linkId
1506
+ });
1507
+ }
1457
1508
  var import_node_fs4, import_node_path4, _config, _requestId, _sessionId, _overrideLock;
1458
1509
  var init_mcp_client = __esm({
1459
1510
  "src/server/mcp-client.ts"() {
@@ -1472,6 +1523,7 @@ var init_mcp_client = __esm({
1472
1523
  var deck_serve_exports = {};
1473
1524
  __export(deck_serve_exports, {
1474
1525
  broadcastToClients: () => broadcastToClients,
1526
+ buildSharePayload: () => buildSharePayload,
1475
1527
  consumeRenderError: () => consumeRenderError,
1476
1528
  createFeedbackWaiter: () => createFeedbackWaiter,
1477
1529
  resolveFeedback: () => resolveFeedback,
@@ -1484,6 +1536,10 @@ function sessionDir(cwd, session) {
1484
1536
  function sessionJsonPath(cwd, session) {
1485
1537
  return import_node_path5.default.join(sessionDir(cwd, session), "session.json");
1486
1538
  }
1539
+ function sessionIdFromCloud(title, fallbackId) {
1540
+ const slug = title.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 80);
1541
+ return slug || `deck-${fallbackId.slice(0, 8)}`;
1542
+ }
1487
1543
  function readPersistedSession(cwd, session) {
1488
1544
  try {
1489
1545
  const raw = import_node_fs5.default.readFileSync(sessionJsonPath(cwd, session), "utf-8");
@@ -1527,7 +1583,11 @@ function listPersistedSessions(cwd) {
1527
1583
  createdAt: persisted.createdAt,
1528
1584
  updatedAt: persisted.updatedAt,
1529
1585
  shared: Boolean(sync),
1530
- stale: Boolean(sync) && persisted.version > (sync?.version ?? 0)
1586
+ stale: Boolean(sync) && persisted.version > (sync?.version ?? 0),
1587
+ shareUrl: sync?.publicShareUrl,
1588
+ course: sync?.course,
1589
+ watching: sync?.watching,
1590
+ watchedFrom: sync?.watchedFrom
1531
1591
  });
1532
1592
  }
1533
1593
  sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
@@ -1555,6 +1615,24 @@ function readSyncRecord(cwd, session) {
1555
1615
  function writeSyncRecord(cwd, session, rec) {
1556
1616
  import_node_fs5.default.writeFileSync(syncJsonPath(cwd, session), JSON.stringify(rec, null, 2));
1557
1617
  }
1618
+ function resolveShareConfig(cwd, course) {
1619
+ if (course) {
1620
+ const override = mcpConfigForProfile(cwd, course);
1621
+ if (!override) {
1622
+ return { error: `Course "${course}" is not a usable profile in .launch-secure.cred.config (missing pat/serverUrl/orgSlug/projectSlug?).` };
1623
+ }
1624
+ return { override };
1625
+ }
1626
+ try {
1627
+ loadMcpConfig(cwd);
1628
+ } catch (err) {
1629
+ return { error: `Cloud sharing needs an active LaunchSecure course (.launch-secure.cred.config) or a "launch-secure" entry in .mcp.json: ${String(err)}` };
1630
+ }
1631
+ return { override: null };
1632
+ }
1633
+ function runWithShareConfig(override, fn) {
1634
+ return override ? withMcpConfig(override, fn) : fn();
1635
+ }
1558
1636
  function escHtml2(s) {
1559
1637
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1560
1638
  }
@@ -1571,31 +1649,49 @@ function readIframeArtifact(cwd, src) {
1571
1649
  return null;
1572
1650
  }
1573
1651
  }
1574
- function renderShareSection(cwd, block) {
1575
- const label = block?.label ? `<h2 class="db-title">${escHtml2(String(block.label))}</h2>` : "";
1652
+ function renderShareSection(cwd, block, showLabel = true) {
1653
+ const label = showLabel && block?.label ? `<h2 class="db-title">${escHtml2(String(block.label))}</h2>` : "";
1576
1654
  switch (block?.type) {
1577
1655
  case "iframe": {
1578
1656
  const doc = readIframeArtifact(cwd, block.src) ?? "<p>(missing artifact)</p>";
1579
- return `<section class="db">${label}<iframe class="db-frame" sandbox="allow-scripts allow-popups" srcdoc="${escAttr(doc)}"></iframe></section>`;
1657
+ return `${label}<iframe class="db-frame" sandbox="allow-scripts allow-popups" srcdoc="${escAttr(doc)}"></iframe>`;
1580
1658
  }
1581
1659
  case "rich-html":
1582
1660
  case "html":
1583
- return `<section class="db">${label}<div class="db-html">${typeof block.content === "string" ? block.content : ""}</div></section>`;
1661
+ return `${label}<div class="db-html">${typeof block.content === "string" ? block.content : ""}</div>`;
1584
1662
  case "markdown":
1585
- return `<section class="db">${label}<div class="db-md" data-md="${escAttr(typeof block.content === "string" ? block.content : "")}"></div></section>`;
1663
+ return `${label}<div class="db-md" data-md="${escAttr(typeof block.content === "string" ? block.content : "")}"></div>`;
1586
1664
  case "mermaid":
1587
- return `<section class="db">${label}<pre class="mermaid">${escHtml2(typeof block.content === "string" ? block.content : "")}</pre></section>`;
1665
+ if (typeof block.renderedSvg === "string" && block.renderedSvg.trim()) {
1666
+ return `${label}<div class="mermaid-svg">${block.renderedSvg}</div>`;
1667
+ }
1668
+ return `${label}<pre class="mermaid">${escHtml2(typeof block.content === "string" ? block.content : "")}</pre>`;
1588
1669
  case "options": {
1589
1670
  const opts = Array.isArray(block.options) ? block.options : [];
1590
- const lis = opts.map(
1591
- (o) => `<li><strong>${escHtml2(String(o?.label ?? o?.id ?? ""))}</strong>${o?.description ? ` \u2014 ${escHtml2(String(o.description))}` : ""}</li>`
1671
+ const cards = opts.map(
1672
+ (o) => `<div class="db-option"><div class="db-option-label">${escHtml2(String(o?.label ?? o?.id ?? ""))}</div>${o?.description ? `<div class="db-option-desc">${escHtml2(String(o.description))}</div>` : ""}${o?.preview ? `<pre class="db-option-pre">${escHtml2(String(o.preview))}</pre>` : ""}</div>`
1592
1673
  ).join("");
1593
- return `<section class="db">${label}<ul>${lis}</ul></section>`;
1674
+ return `${label}<div class="db-options">${cards}</div>`;
1594
1675
  }
1595
1676
  default:
1596
- return `<section class="db">${label}</section>`;
1677
+ return label;
1597
1678
  }
1598
1679
  }
1680
+ function buildSharePayload(cwd, session, blocks, svgByIndex = {}) {
1681
+ const enriched = blocks.map((b, i) => {
1682
+ const svg = svgByIndex[String(i)];
1683
+ return b?.type === "mermaid" && typeof svg === "string" && svg.trim() ? { ...b, renderedSvg: svg } : b;
1684
+ });
1685
+ const html = buildShareHtml(cwd, session, enriched);
1686
+ const cloudBlocks = enriched.map((b) => {
1687
+ if (b?.type === "iframe" && typeof b.src === "string") {
1688
+ const content = readIframeArtifact(cwd, b.src);
1689
+ return content ? { ...b, content } : b;
1690
+ }
1691
+ return b;
1692
+ });
1693
+ return { html, cloudBlocks };
1694
+ }
1599
1695
  function buildShareHtml(cwd, session, blocks) {
1600
1696
  if (blocks.length === 1) {
1601
1697
  const only = blocks[0];
@@ -1605,28 +1701,57 @@ function buildShareHtml(cwd, session, blocks) {
1605
1701
  }
1606
1702
  }
1607
1703
  const list = blocks;
1608
- const sections = list.map((b) => renderShareSection(cwd, b)).join("\n");
1704
+ const multi = list.length > 1;
1705
+ const tabBar = multi ? `<nav class="deck-tabs">${list.map(
1706
+ (b, i) => `<button class="deck-tab${i === 0 ? " active" : ""}" data-tab-btn="${i}">${escHtml2(String(b?.label ?? `Tab ${i + 1}`))}</button>`
1707
+ ).join("")}</nav>` : "";
1708
+ const sections = list.map(
1709
+ (b, i) => `<section class="db" data-tab="${i}"${multi && i > 0 ? " hidden" : ""}>${renderShareSection(cwd, b, !multi)}</section>`
1710
+ ).join("\n");
1609
1711
  const hasMd = list.some((b) => b?.type === "markdown");
1610
- const hasMermaid = list.some((b) => b?.type === "mermaid");
1712
+ const hasUnrenderedMermaid = list.some(
1713
+ (b) => b?.type === "mermaid" && !(typeof b?.renderedSvg === "string" && b.renderedSvg.trim())
1714
+ );
1611
1715
  return `<!DOCTYPE html>
1612
1716
  <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
1613
1717
  <title>${escHtml2(session)}</title>
1718
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,100..1000&display=swap" rel="stylesheet">
1614
1719
  <style>
1615
- body { margin:0; font-family: ui-sans-serif, system-ui, sans-serif; background:#0b0e14; color:#e6e9ef; }
1720
+ body { margin:0; font-family: 'DM Sans', system-ui, sans-serif; background:#0b0e14; color:#e6e9ef; }
1616
1721
  .db { padding:20px 24px; border-bottom:1px solid #1e2433; }
1617
1722
  .db-title { font-size:13px; font-weight:600; margin:0 0 12px; color:#9aa4b2; text-transform:uppercase; letter-spacing:.05em; }
1618
1723
  .db-frame { width:100%; height:70vh; border:0; border-radius:8px; background:#fff; }
1619
1724
  .db-html { background:#fff; color:#111; border-radius:8px; padding:16px; overflow:auto; }
1620
1725
  ul { line-height:1.7; }
1621
- .mermaid { background:#fff; border-radius:8px; padding:16px; }
1726
+ /* The embedded SVG is dark-themed (captured from the deck client); keep its
1727
+ backdrop transparent so it sits on the dark page, matching the in-app
1728
+ DeckView \u2014 not a white card. The CDN-source fallback also renders dark. */
1729
+ .mermaid { background:transparent; border-radius:8px; padding:16px; }
1730
+ .mermaid-svg { background:transparent; border-radius:8px; padding:16px; overflow:auto; }
1731
+ .mermaid-svg svg { max-width:100%; height:auto; }
1732
+ .db-options { display:grid; gap:12px; grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); }
1733
+ .db-option { border:2px solid #1e2433; border-radius:8px; padding:16px; }
1734
+ .db-option-label { font-size:14px; font-weight:600; color:#e6e9ef; }
1735
+ .db-option-desc { margin-top:4px; font-size:12px; color:#9aa4b2; }
1736
+ .db-option-pre { margin-top:8px; padding:8px; border-radius:6px; background:#11151f; font-size:12px; overflow:auto; }
1737
+ .deck-tabs { position:sticky; top:0; z-index:5; display:flex; flex-wrap:wrap; gap:2px; background:#0b0e14; border-bottom:1px solid #1e2433; padding:0 16px; }
1738
+ .deck-tab { appearance:none; border:0; background:transparent; color:#9aa4b2; font:inherit; font-size:13px; font-weight:500; padding:12px 16px; cursor:pointer; border-bottom:2px solid transparent; }
1739
+ .deck-tab:hover { color:#e6e9ef; }
1740
+ .deck-tab.active { color:#c084fc; border-bottom-color:#c084fc; }
1741
+ .db[hidden] { display:none; }
1622
1742
  </style></head>
1623
1743
  <body>
1744
+ ${tabBar}
1624
1745
  ${sections}
1746
+ ${multi ? `<script>(function(){var b=[].slice.call(document.querySelectorAll('[data-tab-btn]')),s=[].slice.call(document.querySelectorAll('[data-tab]'));b.forEach(function(x){x.addEventListener('click',function(){var i=x.getAttribute('data-tab-btn');s.forEach(function(y){y.hidden=y.getAttribute('data-tab')!==i;});b.forEach(function(z){z.classList.toggle('active',z===x);});});});})();</script>` : ""}
1625
1747
  ${hasMd ? `<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
1626
1748
  <script>document.querySelectorAll('.db-md').forEach(function(el){try{el.innerHTML=marked.parse(el.dataset.md||'')}catch(e){el.textContent=el.dataset.md||''}});</script>` : ""}
1627
- ${hasMermaid ? `<script type="module">import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';mermaid.initialize({startOnLoad:true,theme:'dark'});</script>` : ""}
1749
+ ${hasUnrenderedMermaid ? `<script type="module">import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';mermaid.initialize({startOnLoad:true,theme:document.documentElement.classList.contains('light')?'default':'dark',securityLevel:'loose',fontFamily:"'DM Sans', system-ui, sans-serif"});</script>` : ""}
1628
1750
  </body></html>`;
1629
1751
  }
1752
+ function broadcastDiscuss() {
1753
+ broadcastToClients({ type: "discuss_session", sessions: discussSessions.map((d) => d.id) });
1754
+ }
1630
1755
  function consumeRenderError() {
1631
1756
  const err = lastRenderError;
1632
1757
  lastRenderError = null;
@@ -1856,6 +1981,74 @@ async function startDeckServer(opts = {}) {
1856
1981
  jsonResponse(res, 200, { sessions: listPersistedSessions(cwd) });
1857
1982
  return;
1858
1983
  }
1984
+ const pullMatch = url2.pathname.match(/^\/api\/cloud-decks\/([^/]+)\/pull$/);
1985
+ if (pullMatch && req.method === "POST") {
1986
+ const deckId = decodeURIComponent(pullMatch[1]);
1987
+ let course;
1988
+ try {
1989
+ const raw = await readBody(req);
1990
+ if (raw.trim()) {
1991
+ const parsed = JSON.parse(raw);
1992
+ course = parsed.course?.trim() || void 0;
1993
+ }
1994
+ } catch {
1995
+ }
1996
+ const cfg = resolveShareConfig(cwd, course);
1997
+ if ("error" in cfg) {
1998
+ jsonResponse(res, 400, { ok: false, error: cfg.error });
1999
+ return;
2000
+ }
2001
+ try {
2002
+ const deck = await runWithShareConfig(cfg.override, () => readCloudDeck(deckId));
2003
+ if (!deck) {
2004
+ jsonResponse(res, 404, { ok: false, error: `No shared deck "${deckId}" in this course.` });
2005
+ return;
2006
+ }
2007
+ if (!deck.html && !deck.blocks) {
2008
+ jsonResponse(res, 422, { ok: false, error: "That deck carries no viewable content (no deckHtml or deckBlocks)." });
2009
+ return;
2010
+ }
2011
+ const session = sessionIdFromCloud(deck.title, deck.id);
2012
+ const sessionEncoded = encodeURIComponent(session);
2013
+ let blocks;
2014
+ if (deck.html) {
2015
+ const dir = sessionDir(cwd, session);
2016
+ import_node_fs5.default.mkdirSync(dir, { recursive: true });
2017
+ import_node_fs5.default.writeFileSync(import_node_path5.default.join(dir, "imported.html"), deck.html);
2018
+ blocks = [{ type: "iframe", label: deck.title, src: `/deck-files/${sessionEncoded}/imported.html` }];
2019
+ } else {
2020
+ blocks = deck.blocks;
2021
+ }
2022
+ const persisted = writePersistedSession(cwd, session, { mode: "show", blocks });
2023
+ writeSyncRecord(cwd, session, {
2024
+ resourceId: deck.id,
2025
+ sharedAt: (/* @__PURE__ */ new Date()).toISOString(),
2026
+ version: persisted.version,
2027
+ course,
2028
+ watching: true,
2029
+ watchedFrom: deck.author
2030
+ });
2031
+ broadcastToClients({
2032
+ type: "session",
2033
+ session,
2034
+ mode: "show",
2035
+ blocks,
2036
+ version: persisted.version
2037
+ });
2038
+ jsonResponse(res, 200, {
2039
+ ok: true,
2040
+ session,
2041
+ deckId: deck.id,
2042
+ title: deck.title,
2043
+ author: deck.author,
2044
+ blockCount: blocks.length,
2045
+ course
2046
+ });
2047
+ } catch (err) {
2048
+ jsonResponse(res, 500, { ok: false, error: String(err) });
2049
+ }
2050
+ return;
2051
+ }
1859
2052
  const shareMatch = url2.pathname.match(/^\/api\/sessions\/([^/]+)\/share$/);
1860
2053
  if (shareMatch && req.method === "POST") {
1861
2054
  const id = decodeURIComponent(shareMatch[1]);
@@ -1865,45 +2058,35 @@ async function startDeckServer(opts = {}) {
1865
2058
  return;
1866
2059
  }
1867
2060
  let course;
2061
+ let svgByIndex = {};
2062
+ let publicLink = false;
1868
2063
  try {
1869
2064
  const raw = await readBody(req);
1870
2065
  if (raw.trim()) {
1871
- course = JSON.parse(raw).course?.trim() || void 0;
2066
+ const parsed = JSON.parse(raw);
2067
+ course = parsed.course?.trim() || void 0;
2068
+ if (parsed.svgByIndex && typeof parsed.svgByIndex === "object") svgByIndex = parsed.svgByIndex;
2069
+ publicLink = parsed.publicLink === true;
1872
2070
  }
1873
2071
  } catch {
1874
2072
  }
1875
- let override = null;
1876
- if (course) {
1877
- override = mcpConfigForProfile(cwd, course);
1878
- if (!override) {
1879
- jsonResponse(res, 400, {
1880
- ok: false,
1881
- error: `Course "${course}" is not a usable profile in .launch-secure.cred.config (missing pat/serverUrl/orgSlug/projectSlug?).`
1882
- });
1883
- return;
1884
- }
1885
- } else {
1886
- try {
1887
- loadMcpConfig(cwd);
1888
- } catch (err) {
1889
- jsonResponse(res, 400, {
1890
- ok: false,
1891
- error: `Cloud sharing needs an active LaunchSecure course (.launch-secure.cred.config) or a "launch-secure" entry in .mcp.json: ${String(err)}`
1892
- });
1893
- return;
1894
- }
2073
+ const cfg = resolveShareConfig(cwd, course);
2074
+ if ("error" in cfg) {
2075
+ jsonResponse(res, 400, { ok: false, error: cfg.error });
2076
+ return;
1895
2077
  }
2078
+ const { override } = cfg;
1896
2079
  try {
1897
2080
  const blocks = Array.isArray(persisted.blocks) ? persisted.blocks : [];
1898
- const html = buildShareHtml(cwd, id, blocks);
2081
+ const { html, cloudBlocks } = buildSharePayload(cwd, id, blocks, svgByIndex);
1899
2082
  const types = blocks.map((b) => b?.type).filter(Boolean);
1900
2083
  const body = `Shared deck "${id}" \u2014 ${blocks.length} block${blocks.length === 1 ? "" : "s"}${types.length ? ` (${types.join(", ")})` : ""}. Open the Decks tab in the Communication Center to view.`;
1901
- const input = { title: id, body, html, sessionId: id, blockCount: blocks.length };
1902
- const existing2 = readSyncRecord(cwd, id);
2084
+ const input = { title: id, body, html, blocks: cloudBlocks, sessionId: id, blockCount: blocks.length };
2085
+ const existingSync = readSyncRecord(cwd, id);
1903
2086
  const doShare = async () => {
1904
- if (existing2?.resourceId) {
2087
+ if (existingSync?.resourceId) {
1905
2088
  try {
1906
- const { id: rid2 } = await updateDeck(existing2.resourceId, input);
2089
+ const { id: rid2 } = await updateDeck(existingSync.resourceId, input);
1907
2090
  return { resourceId: rid2, updated: true };
1908
2091
  } catch {
1909
2092
  const { id: rid2 } = await writeDeck(input);
@@ -1913,14 +2096,89 @@ async function startDeckServer(opts = {}) {
1913
2096
  const { id: rid } = await writeDeck(input);
1914
2097
  return { resourceId: rid, updated: false };
1915
2098
  };
1916
- const { resourceId, updated } = override ? await withMcpConfig(override, doShare) : await doShare();
2099
+ const result = await runWithShareConfig(override, async () => {
2100
+ const shared = await doShare();
2101
+ let link = null;
2102
+ let linkError;
2103
+ if (publicLink) {
2104
+ try {
2105
+ const minted = await createDeckShareLink(shared.resourceId);
2106
+ link = { shareUrl: minted.shareUrl, linkId: minted.linkId };
2107
+ } catch (err) {
2108
+ linkError = err instanceof Error ? err.message : String(err);
2109
+ }
2110
+ }
2111
+ return { shared, link, linkError };
2112
+ });
2113
+ if (publicLink && result.linkError && !result.shared.updated) {
2114
+ try {
2115
+ await runWithShareConfig(override, () => deleteDeck(result.shared.resourceId));
2116
+ } catch {
2117
+ }
2118
+ jsonResponse(res, 200, {
2119
+ ok: false,
2120
+ error: `Couldn't create the public link, so the deck wasn't shared: ${result.linkError}`
2121
+ });
2122
+ return;
2123
+ }
2124
+ const publicShareUrl = result.link?.shareUrl ?? existingSync?.publicShareUrl;
2125
+ const publicLinkId = result.link?.linkId ?? existingSync?.publicLinkId;
1917
2126
  writeSyncRecord(cwd, id, {
1918
- resourceId,
2127
+ resourceId: result.shared.resourceId,
1919
2128
  sharedAt: (/* @__PURE__ */ new Date()).toISOString(),
1920
2129
  version: persisted.version,
1921
- course
2130
+ course,
2131
+ ...publicShareUrl ? { publicShareUrl } : {},
2132
+ ...publicLinkId ? { publicLinkId } : {}
2133
+ });
2134
+ jsonResponse(res, 200, {
2135
+ ok: true,
2136
+ resourceId: result.shared.resourceId,
2137
+ updated: result.shared.updated,
2138
+ course,
2139
+ shareUrl: publicShareUrl ?? null,
2140
+ ...result.linkError ? { linkWarning: result.linkError } : {}
1922
2141
  });
1923
- jsonResponse(res, 200, { ok: true, resourceId, updated, course });
2142
+ } catch (err) {
2143
+ jsonResponse(res, 500, { ok: false, error: String(err) });
2144
+ }
2145
+ return;
2146
+ }
2147
+ const publicLinkMatch = url2.pathname.match(/^\/api\/sessions\/([^/]+)\/public-link$/);
2148
+ if (publicLinkMatch && (req.method === "POST" || req.method === "DELETE")) {
2149
+ const id = decodeURIComponent(publicLinkMatch[1]);
2150
+ const sync = readSyncRecord(cwd, id);
2151
+ if (!sync?.resourceId) {
2152
+ jsonResponse(res, 409, { ok: false, error: "Share this deck to the cloud first, then create a public link." });
2153
+ return;
2154
+ }
2155
+ const cfg = resolveShareConfig(cwd, sync.course);
2156
+ if ("error" in cfg) {
2157
+ jsonResponse(res, 400, { ok: false, error: cfg.error });
2158
+ return;
2159
+ }
2160
+ const { override } = cfg;
2161
+ if (req.method === "DELETE") {
2162
+ if (!sync.publicLinkId) {
2163
+ if (sync.publicShareUrl) {
2164
+ writeSyncRecord(cwd, id, { ...sync, publicShareUrl: void 0, publicLinkId: void 0 });
2165
+ }
2166
+ jsonResponse(res, 200, { ok: true, revoked: false });
2167
+ return;
2168
+ }
2169
+ try {
2170
+ await runWithShareConfig(override, () => revokeDeckShareLink(sync.resourceId, sync.publicLinkId));
2171
+ writeSyncRecord(cwd, id, { ...sync, publicShareUrl: void 0, publicLinkId: void 0 });
2172
+ jsonResponse(res, 200, { ok: true, revoked: true });
2173
+ } catch (err) {
2174
+ jsonResponse(res, 500, { ok: false, error: String(err) });
2175
+ }
2176
+ return;
2177
+ }
2178
+ try {
2179
+ const minted = await runWithShareConfig(override, () => createDeckShareLink(sync.resourceId));
2180
+ writeSyncRecord(cwd, id, { ...sync, publicShareUrl: minted.shareUrl, publicLinkId: minted.linkId });
2181
+ jsonResponse(res, 200, { ok: true, shareUrl: minted.shareUrl });
1924
2182
  } catch (err) {
1925
2183
  jsonResponse(res, 500, { ok: false, error: String(err) });
1926
2184
  }
@@ -1948,7 +2206,16 @@ async function startDeckServer(opts = {}) {
1948
2206
  }
1949
2207
  jsonResponse(res, 200, { ok: true, deleted: true });
1950
2208
  } catch (err) {
1951
- jsonResponse(res, 500, { ok: false, error: String(err) });
2209
+ const msg = err instanceof Error ? err.message : String(err);
2210
+ if (/not\s*found/i.test(msg)) {
2211
+ try {
2212
+ import_node_fs5.default.rmSync(syncJsonPath(cwd, id), { force: true });
2213
+ } catch {
2214
+ }
2215
+ jsonResponse(res, 200, { ok: true, deleted: false, reason: "already removed" });
2216
+ return;
2217
+ }
2218
+ jsonResponse(res, 500, { ok: false, error: msg });
1952
2219
  }
1953
2220
  return;
1954
2221
  }
@@ -2039,6 +2306,10 @@ async function startDeckServer(opts = {}) {
2039
2306
  deletePersistedSession(cwd, session);
2040
2307
  } catch {
2041
2308
  }
2309
+ const beforePins = discussSessions.length;
2310
+ discussSessions = session === "all" ? [] : discussSessions.filter((d) => d.id !== session);
2311
+ if (discussSessions.length !== beforePins) broadcastDiscuss();
2312
+ if (session === "all" || activeSession?.id === session) activeSession = null;
2042
2313
  jsonResponse(res, 200, { ok: true });
2043
2314
  return;
2044
2315
  }
@@ -2047,6 +2318,16 @@ async function startDeckServer(opts = {}) {
2047
2318
  jsonResponse(res, 200, err ?? null);
2048
2319
  return;
2049
2320
  }
2321
+ if (req.method === "GET" && url2.pathname === "/api/active-session") {
2322
+ jsonResponse(res, 200, {
2323
+ discuss: discussSessions.map((d) => d.id),
2324
+ // ordered = pin number
2325
+ active: activeSession?.id ?? null,
2326
+ focused: activeSession?.focused ?? false,
2327
+ activeAt: activeSession?.at ?? null
2328
+ });
2329
+ return;
2330
+ }
2050
2331
  if (req.method === "GET" && url2.pathname.startsWith("/deck-files/")) {
2051
2332
  const relative = decodeURIComponent(url2.pathname.slice("/deck-files/".length));
2052
2333
  if (relative.includes("..") || relative.startsWith("/")) {
@@ -2119,6 +2400,24 @@ async function startDeckServer(opts = {}) {
2119
2400
  source: msg.source ?? ""
2120
2401
  };
2121
2402
  }
2403
+ if (msg.type === "active_session" && msg.session) {
2404
+ activeSession = {
2405
+ id: msg.session,
2406
+ focused: msg.focused ?? false,
2407
+ at: Date.now()
2408
+ };
2409
+ }
2410
+ if (msg.type === "discuss_session" && msg.session) {
2411
+ const id = msg.session;
2412
+ const idx = discussSessions.findIndex((d) => d.id === id);
2413
+ if (idx >= 0) discussSessions.splice(idx, 1);
2414
+ else discussSessions.push({ id, at: Date.now() });
2415
+ broadcastDiscuss();
2416
+ }
2417
+ if (msg.type === "discuss_clear_all") {
2418
+ discussSessions = [];
2419
+ broadcastDiscuss();
2420
+ }
2122
2421
  } catch {
2123
2422
  }
2124
2423
  });
@@ -2184,7 +2483,7 @@ function runServeCli(argv) {
2184
2483
  process.exit(1);
2185
2484
  });
2186
2485
  }
2187
- var import_node_http, import_node_fs5, import_node_path5, import_ws, DEFAULT_PORT, MAX_PORT_SCAN, MIME_TYPES, pendingFeedback, lastRenderError, wss;
2486
+ var import_node_http, import_node_fs5, import_node_path5, import_ws, DEFAULT_PORT, MAX_PORT_SCAN, MIME_TYPES, pendingFeedback, lastRenderError, activeSession, discussSessions, wss;
2188
2487
  var init_deck_serve = __esm({
2189
2488
  "src/server/deck-serve.ts"() {
2190
2489
  "use strict";
@@ -2214,6 +2513,8 @@ var init_deck_serve = __esm({
2214
2513
  };
2215
2514
  pendingFeedback = /* @__PURE__ */ new Map();
2216
2515
  lastRenderError = null;
2516
+ activeSession = null;
2517
+ discussSessions = [];
2217
2518
  wss = null;
2218
2519
  }
2219
2520
  });
@@ -2227,7 +2528,7 @@ function httpGet(url) {
2227
2528
  return new Promise((resolve, reject) => {
2228
2529
  const parsed = new URL(url);
2229
2530
  const req = import_node_http2.default.request(
2230
- { hostname: parsed.hostname, port: parsed.port, path: parsed.pathname, method: "GET" },
2531
+ { hostname: parsed.hostname, port: parsed.port, path: parsed.pathname + parsed.search, method: "GET" },
2231
2532
  (res) => {
2232
2533
  let buf = "";
2233
2534
  res.on("data", (chunk) => {
@@ -2473,6 +2774,30 @@ async function handleTool(name, args) {
2473
2774
  }
2474
2775
  return text(JSON.stringify({ running: false }));
2475
2776
  }
2777
+ case "active_session": {
2778
+ const lock = getLiveLock(projectRoot);
2779
+ if (!lock) {
2780
+ return text(JSON.stringify({
2781
+ error: "LaunchDeck server is not running \u2014 nothing is open.",
2782
+ hint: "Start it with start_server, or ask the user to open the deck."
2783
+ }));
2784
+ }
2785
+ try {
2786
+ const resp = await fetch(`${lock.url}/api/active-session`, {
2787
+ signal: AbortSignal.timeout(3e3)
2788
+ });
2789
+ const data = await resp.json();
2790
+ const pins = data.discuss ?? [];
2791
+ const targets = pins.length ? pins : data.active ? [data.active] : [];
2792
+ return text(JSON.stringify({
2793
+ ...data,
2794
+ targets,
2795
+ targetSource: pins.length ? "pinned" : data.active ? "active-tab" : "none"
2796
+ }));
2797
+ } catch (err) {
2798
+ return text(JSON.stringify({ error: `Failed to read active session: ${err}` }));
2799
+ }
2800
+ }
2476
2801
  case "generate_contract": {
2477
2802
  const session = args.session;
2478
2803
  const dir = (0, import_node_path6.join)(projectRoot, LAUNCHSECURE_DIR, "deck-files", session);
@@ -2495,6 +2820,31 @@ async function handleTool(name, args) {
2495
2820
  return text(JSON.stringify({ error: `Failed to generate contract: ${err}` }));
2496
2821
  }
2497
2822
  }
2823
+ case "deck_pull": {
2824
+ const lock = getLiveLock(projectRoot);
2825
+ if (!lock) {
2826
+ return text(JSON.stringify({ error: "LaunchDeck server is not running. Call start_server first." }));
2827
+ }
2828
+ const deckId = args.deck?.trim();
2829
+ if (!deckId) {
2830
+ return text(JSON.stringify({ error: "Provide the deck's comment id (from launch-secure deck_list / watched_decks)." }));
2831
+ }
2832
+ const course = args.course?.trim();
2833
+ try {
2834
+ const resp = await httpPost(`${lock.url}/api/cloud-decks/${encodeURIComponent(deckId)}/pull`, course ? { course } : {});
2835
+ const parsed = JSON.parse(resp);
2836
+ if (parsed.ok === false) {
2837
+ return text(JSON.stringify({ error: parsed.error ?? "Failed to pull the deck." }));
2838
+ }
2839
+ return text(JSON.stringify({
2840
+ ...parsed,
2841
+ url: lock.url,
2842
+ action: "Deck pulled into a local tab. Open the LaunchDeck browser to view it; it can now be \u{1F441}-watched like any local deck."
2843
+ }));
2844
+ } catch (err) {
2845
+ return text(JSON.stringify({ error: `Failed to pull deck: ${err}` }));
2846
+ }
2847
+ }
2498
2848
  default:
2499
2849
  return text(`Unknown tool: ${name}`);
2500
2850
  }
@@ -2713,6 +3063,11 @@ var init_deck_mcp = __esm({
2713
3063
  description: "Check whether the LaunchDeck UI server is running.",
2714
3064
  inputSchema: { type: "object", properties: {} }
2715
3065
  },
3066
+ {
3067
+ name: "active_session",
3068
+ description: 'Find out which deck(s) the user is currently looking at or watching. Returns `discuss` (an ORDERED list of sessions the user is explicitly WATCHING via the \u{1F441} eye button \u2014 order = the number shown in the UI; the strongest signal, use these when non-empty), `active` (the tab last shown in the browser), `focused` (whether the deck window itself was focused when `active` was reported \u2014 if false, the user is probably elsewhere and `active` is just the last-viewed tab), and `targets` (the resolved answer to "which deck(s) does the user mean": the watched decks if any, else the active tab). Use this instead of guessing from the session list when the user says "this deck" / "the open one" / "the ones I\'m watching".',
3069
+ inputSchema: { type: "object", properties: {} }
3070
+ },
2716
3071
  {
2717
3072
  name: "generate_contract",
2718
3073
  description: "Generate a contract from a previously pushed blast-radius session.\n\nReads the blast radius manifest from the session's saved files and produces a structured contract with actionable items, acceptance criteria, dependencies, and a verify-unchanged checklist.\n\nReturns JSON contract. Also saves contract.json and contract.md to the session directory.",
@@ -2726,6 +3081,24 @@ var init_deck_mcp = __esm({
2726
3081
  },
2727
3082
  required: ["session"]
2728
3083
  }
3084
+ },
3085
+ {
3086
+ name: "deck_pull",
3087
+ description: "Pull a cloud-shared deck DOWN into a local LaunchDeck session tab \u2014 the inverse of publishing a deck up. After this, the deck is an ordinary local tab the user can view, \u{1F441}-watch, download, or re-pull. Takes the deck's comment id \u2014 get it from launch-secure's deck_list or watched_decks. The deck is reconstructed from its shared HTML snapshot as one self-contained block (view-faithful; not re-editable block-by-block). Requires the LaunchDeck server to be running (start_server first). Note: listing shared decks and reading watch state live on the launch-secure MCP (deck_list / watched_decks), not here.",
3088
+ inputSchema: {
3089
+ type: "object",
3090
+ properties: {
3091
+ deck: {
3092
+ type: "string",
3093
+ description: "Deck comment id (from launch-secure deck_list / watched_decks)."
3094
+ },
3095
+ course: {
3096
+ type: "string",
3097
+ description: "Optional course (cred profile) to read from. Omit for the active course."
3098
+ }
3099
+ },
3100
+ required: ["deck"]
3101
+ }
2729
3102
  }
2730
3103
  ];
2731
3104
  }