@launchsecure/launch-kit 0.0.39 → 0.0.41

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 (113) 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-jjBWyhry.js → index-DkTFX53U.js} +1 -1
  7. package/dist/council-client/index.html +2 -2
  8. package/dist/deck-client/assets/_baseUniq-mvYvzeEJ.js +1 -0
  9. package/dist/deck-client/assets/arc-CX4ylnp2.js +1 -0
  10. package/dist/deck-client/assets/architectureDiagram-Q4EWVU46-BkR-5IRK.js +36 -0
  11. package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-CwAGy9lU.js → blockDiagram-DXYQGD6D-DVNQht7c.js} +2 -2
  12. package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-L_g_SS21.js → c4Diagram-AHTNJAMY-Cbq1rlG8.js} +2 -2
  13. package/dist/deck-client/assets/channel-B9GC-CLn.js +1 -0
  14. package/dist/deck-client/assets/chunk-4BX2VUAB-D58Co4lU.js +1 -0
  15. package/dist/deck-client/assets/{chunk-4TB4RGXK-Bk0FUbxU.js → chunk-4TB4RGXK-BYvhTm3d.js} +1 -1
  16. package/dist/deck-client/assets/chunk-55IACEB6-oWukUhYg.js +1 -0
  17. package/dist/deck-client/assets/chunk-EDXVE4YY-Cm58kVnZ.js +1 -0
  18. package/dist/deck-client/assets/{chunk-FMBD7UC4-DqOvWr1k.js → chunk-FMBD7UC4-Dg-i7kzi.js} +1 -1
  19. package/dist/deck-client/assets/{chunk-OYMX7WX6-1Kd7yK5u.js → chunk-OYMX7WX6-C72wigPl.js} +1 -1
  20. package/dist/deck-client/assets/chunk-QZHKN3VN-CLgeuAKw.js +1 -0
  21. package/dist/deck-client/assets/chunk-YZCP3GAM-HDDlJ5oI.js +1 -0
  22. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-CFBvYQ9j.js +1 -0
  23. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-CFBvYQ9j.js +1 -0
  24. package/dist/deck-client/assets/clone-n-WQlAGe.js +1 -0
  25. package/dist/deck-client/assets/cose-bilkent-S5V4N54A-CUXQKg2M.js +1 -0
  26. package/dist/deck-client/assets/dagre-KV5264BT-C5M-fVDc.js +4 -0
  27. package/dist/deck-client/assets/diagram-5BDNPKRD-CcVsQ0S8.js +10 -0
  28. package/dist/deck-client/assets/diagram-G4DWMVQ6-DJswXyep.js +24 -0
  29. package/dist/deck-client/assets/diagram-MMDJMWI5-CGT76fm1.js +43 -0
  30. package/dist/deck-client/assets/diagram-TYMM5635-BBsYUNN6.js +24 -0
  31. package/dist/deck-client/assets/{erDiagram-SMLLAGMA-aiv9GZnL.js → erDiagram-SMLLAGMA-DKWYEHQS.js} +2 -2
  32. package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-C6Fhvtsy.js → flowDiagram-DWJPFMVM-DLuDYIKT.js} +2 -2
  33. package/dist/deck-client/assets/ganttDiagram-T4ZO3ILL-B19b6Qtj.js +292 -0
  34. package/dist/deck-client/assets/gitGraphDiagram-UUTBAWPF-BYLAfYVS.js +106 -0
  35. package/dist/deck-client/assets/graph-CfzQUfPh.js +1 -0
  36. package/dist/deck-client/assets/index-DlwdTgE_.js +892 -0
  37. package/dist/deck-client/assets/index-evAPhGvM.css +1 -0
  38. package/dist/deck-client/assets/infoDiagram-42DDH7IO-Dp3mUA9c.js +2 -0
  39. package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-BwCUmUVt.js → ishikawaDiagram-UXIWVN3A-BhrNX_jI.js} +5 -5
  40. package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-C6qoqJmJ.js → journeyDiagram-VCZTEJTY-B5lJI492.js} +2 -2
  41. package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-Dz1Tt3sA.js → kanban-definition-6JOO6SKY-D9-lmhQf.js} +2 -2
  42. package/dist/deck-client/assets/layout-CfIe_Su8.js +1 -0
  43. package/dist/deck-client/assets/linear-09ZFRoh_.js +1 -0
  44. package/dist/deck-client/assets/mermaid.core-BaQyIOvj.js +309 -0
  45. package/dist/deck-client/assets/min-CYwCzYaL.js +1 -0
  46. package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-CfXcK1qH.js → mindmap-definition-QFDTVHPH-CouFxf6C.js} +2 -2
  47. package/dist/deck-client/assets/pieDiagram-DEJITSTG-DMB1ufC0.js +30 -0
  48. package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-CXwvZ1i1.js → quadrantDiagram-34T5L4WZ-CBiOKudN.js} +2 -2
  49. package/dist/deck-client/assets/{requirementDiagram-MS252O5E-Cl6xm0fR.js → requirementDiagram-MS252O5E-BMc3GJkx.js} +2 -2
  50. package/dist/deck-client/assets/sankeyDiagram-XADWPNL6-CxACUncm.js +10 -0
  51. package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-BC1MYBn6.js → sequenceDiagram-FGHM5R23-Ch-P3Mzc.js} +2 -2
  52. package/dist/deck-client/assets/stateDiagram-FHFEXIEX-Cy8n7Yzk.js +1 -0
  53. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-C14VKCzi.js +1 -0
  54. package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-DKnITsD4.js → timeline-definition-GMOUNBTQ-C2V4sSkm.js} +2 -2
  55. package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-BdajXRrh.js → vennDiagram-DHZGUBPP-YOqt4VbE.js} +2 -2
  56. package/dist/deck-client/assets/wardley-RL74JXVD-Bxo5x40D.js +162 -0
  57. package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-B2hDCDl2.js → wardleyDiagram-NUSXRM2D-DW9SOqbx.js} +2 -2
  58. package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-CvnYFs51.js → xychartDiagram-5P7HB3ND-D-rZvZOL.js} +2 -2
  59. package/dist/deck-client/index.html +2 -2
  60. package/dist/server/beacon-monitor-entry.js +106 -24
  61. package/dist/server/chart-serve.js +544 -247
  62. package/dist/server/cli.js +1016 -324
  63. package/dist/server/council-entry.js +23 -4
  64. package/dist/server/council-serve.js +22 -3
  65. package/dist/server/deck-mcp-entry.js +523 -125
  66. package/dist/server/deck-serve.js +326 -40
  67. package/dist/server/graph-mcp-entry.js +1160 -357
  68. package/dist/server/init-entry.js +17 -7
  69. package/dist/server/orbit-entry.js +91 -7
  70. package/dist/server/recall-entry.js +94 -12
  71. package/dist/server/rover-entry.js +1 -1
  72. package/package.json +1 -1
  73. package/scaffolds/ls-marketplace/plugins/kit/skills/comms/SKILL.md +34 -1
  74. package/scaffolds/ls-marketplace/plugins/kit/skills/gen-test/SKILL.md +126 -0
  75. package/scaffolds/statusline/statusline-mcp.sh +68 -19
  76. package/scaffolds/statusline/statusline-wrapper.sh +12 -9
  77. package/dist/chart-client/assets/index-ysGpLeOW.css +0 -1
  78. package/dist/client/assets/index-CMN3tlGP.css +0 -32
  79. package/dist/council-client/assets/index-ChmNX6bZ.css +0 -1
  80. package/dist/deck-client/assets/_baseUniq-DOrnEQMI.js +0 -1
  81. package/dist/deck-client/assets/arc-DOWK7V3m.js +0 -1
  82. package/dist/deck-client/assets/architectureDiagram-Q4EWVU46-DPhzvk7q.js +0 -36
  83. package/dist/deck-client/assets/channel-DqiACUUq.js +0 -1
  84. package/dist/deck-client/assets/chunk-4BX2VUAB-RKm0LXpu.js +0 -1
  85. package/dist/deck-client/assets/chunk-55IACEB6-Cl3hja-M.js +0 -1
  86. package/dist/deck-client/assets/chunk-EDXVE4YY-CNIMQCV2.js +0 -1
  87. package/dist/deck-client/assets/chunk-QZHKN3VN-6_kraYpP.js +0 -1
  88. package/dist/deck-client/assets/chunk-YZCP3GAM-FgAwIWlo.js +0 -1
  89. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-D23cq2C3.js +0 -1
  90. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-D23cq2C3.js +0 -1
  91. package/dist/deck-client/assets/clone-C7jSigGq.js +0 -1
  92. package/dist/deck-client/assets/cose-bilkent-S5V4N54A-CigVnnPr.js +0 -1
  93. package/dist/deck-client/assets/dagre-KV5264BT-DHZXTktX.js +0 -4
  94. package/dist/deck-client/assets/diagram-5BDNPKRD-H5k0eauU.js +0 -10
  95. package/dist/deck-client/assets/diagram-G4DWMVQ6-Bg3dFhSY.js +0 -24
  96. package/dist/deck-client/assets/diagram-MMDJMWI5-CQLC410N.js +0 -43
  97. package/dist/deck-client/assets/diagram-TYMM5635-DFTCHVkP.js +0 -24
  98. package/dist/deck-client/assets/ganttDiagram-T4ZO3ILL-DSaGMPM4.js +0 -292
  99. package/dist/deck-client/assets/gitGraphDiagram-UUTBAWPF-DMjL2Vix.js +0 -106
  100. package/dist/deck-client/assets/graph-B7Vn5lkK.js +0 -1
  101. package/dist/deck-client/assets/index-BD36e-tD.js +0 -1196
  102. package/dist/deck-client/assets/index-CGbNOpk9.css +0 -1
  103. package/dist/deck-client/assets/infoDiagram-42DDH7IO-mNi4iygG.js +0 -2
  104. package/dist/deck-client/assets/layout-CZTyRhOG.js +0 -1
  105. package/dist/deck-client/assets/linear--7n7iEvd.js +0 -1
  106. package/dist/deck-client/assets/min-Bh130DN8.js +0 -1
  107. package/dist/deck-client/assets/pieDiagram-DEJITSTG-DjVHLAVw.js +0 -30
  108. package/dist/deck-client/assets/sankeyDiagram-XADWPNL6-BOH9sLyh.js +0 -10
  109. package/dist/deck-client/assets/stateDiagram-FHFEXIEX-kNp9bv8K.js +0 -1
  110. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-hRsAFc2t.js +0 -1
  111. package/dist/deck-client/assets/wardley-RL74JXVD-BL802-su.js +0 -162
  112. /package/dist/chart-client/assets/{index-BlsuXuQ1.js → index-CrYM1-ac.js} +0 -0
  113. /package/dist/client/assets/{index-BA7BHBWT.js → index-BoIjawzY.js} +0 -0
@@ -128,24 +128,24 @@ var init_deck_lockfile = __esm({
128
128
 
129
129
  // src/server/deck-config.ts
130
130
  function loadDeckConfig(rootDir) {
131
- const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
132
- if (!(0, import_node_fs2.existsSync)(configPath)) return {};
131
+ const configPath = (0, import_node_path3.join)(rootDir, CONFIG_FILENAME);
132
+ if (!(0, import_node_fs3.existsSync)(configPath)) return {};
133
133
  try {
134
- return JSON.parse((0, import_node_fs2.readFileSync)(configPath, "utf-8"));
134
+ return JSON.parse((0, import_node_fs3.readFileSync)(configPath, "utf-8"));
135
135
  } catch {
136
136
  return {};
137
137
  }
138
138
  }
139
139
  function saveDeckConfig(rootDir, config) {
140
- const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
141
- (0, import_node_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
140
+ const configPath = (0, import_node_path3.join)(rootDir, CONFIG_FILENAME);
141
+ (0, import_node_fs3.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
142
142
  }
143
- var import_node_fs2, import_node_path2, CONFIG_FILENAME;
143
+ var import_node_fs3, import_node_path3, CONFIG_FILENAME;
144
144
  var init_deck_config = __esm({
145
145
  "src/server/deck-config.ts"() {
146
146
  "use strict";
147
- import_node_fs2 = require("node:fs");
148
- import_node_path2 = require("node:path");
147
+ import_node_fs3 = require("node:fs");
148
+ import_node_path3 = require("node:path");
149
149
  CONFIG_FILENAME = ".launchdeck.json";
150
150
  }
151
151
  });
@@ -1250,7 +1250,7 @@ var init_cred_shape = __esm({
1250
1250
  });
1251
1251
 
1252
1252
  // src/server/mcp-client.ts
1253
- function mcpConfigFromCourse(projectRoot) {
1253
+ function mcpConfigForProfile(projectRoot, name) {
1254
1254
  let cred;
1255
1255
  try {
1256
1256
  cred = readCredFile(projectRoot);
@@ -1260,7 +1260,7 @@ function mcpConfigFromCourse(projectRoot) {
1260
1260
  if (!cred) return null;
1261
1261
  const nested = toNested(cred);
1262
1262
  if (!nested) return null;
1263
- const profile = nested.profiles[nested.active];
1263
+ const profile = nested.profiles[name];
1264
1264
  if (!profile?.pat || !profile.serverUrl || !profile.orgSlug || !profile.projectSlug) {
1265
1265
  return null;
1266
1266
  }
@@ -1273,6 +1273,37 @@ function mcpConfigFromCourse(projectRoot) {
1273
1273
  }
1274
1274
  };
1275
1275
  }
1276
+ function mcpConfigFromCourse(projectRoot) {
1277
+ let cred;
1278
+ try {
1279
+ cred = readCredFile(projectRoot);
1280
+ } catch {
1281
+ return null;
1282
+ }
1283
+ if (!cred) return null;
1284
+ const nested = toNested(cred);
1285
+ if (!nested) return null;
1286
+ return mcpConfigForProfile(projectRoot, nested.active);
1287
+ }
1288
+ function listCourses(projectRoot) {
1289
+ let cred;
1290
+ try {
1291
+ cred = readCredFile(projectRoot);
1292
+ } catch {
1293
+ return [];
1294
+ }
1295
+ if (!cred) return [];
1296
+ const nested = toNested(cred);
1297
+ if (!nested) return [];
1298
+ return Object.entries(nested.profiles).map(([name, p]) => ({
1299
+ name,
1300
+ org: p.orgSlug ?? "",
1301
+ project: p.projectSlug ?? "",
1302
+ serverUrl: p.serverUrl ?? "",
1303
+ active: name === nested.active,
1304
+ usable: Boolean(p.pat && p.serverUrl && p.orgSlug && p.projectSlug)
1305
+ }));
1306
+ }
1276
1307
  function loadMcpConfig(projectRoot) {
1277
1308
  if (_config) return _config;
1278
1309
  const fromCourse = mcpConfigFromCourse(projectRoot);
@@ -1280,13 +1311,13 @@ function loadMcpConfig(projectRoot) {
1280
1311
  _config = fromCourse;
1281
1312
  return _config;
1282
1313
  }
1283
- const mcpPath = (0, import_node_path3.join)(projectRoot, ".mcp.json");
1284
- if (!(0, import_node_fs3.existsSync)(mcpPath)) {
1314
+ const mcpPath = (0, import_node_path4.join)(projectRoot, ".mcp.json");
1315
+ if (!(0, import_node_fs4.existsSync)(mcpPath)) {
1285
1316
  throw new Error(
1286
1317
  `No active course in ${CONFIG_FILENAME2} and no .mcp.json at ${mcpPath}. Run \`launch-course\` or bootstrap with \`launch-kit init\`.`
1287
1318
  );
1288
1319
  }
1289
- const raw = JSON.parse((0, import_node_fs3.readFileSync)(mcpPath, "utf-8"));
1320
+ const raw = JSON.parse((0, import_node_fs4.readFileSync)(mcpPath, "utf-8"));
1290
1321
  const entry = raw.mcpServers["launch-secure"];
1291
1322
  if (!entry?.url) {
1292
1323
  throw new Error(
@@ -1299,6 +1330,23 @@ function loadMcpConfig(projectRoot) {
1299
1330
  };
1300
1331
  return _config;
1301
1332
  }
1333
+ function withMcpConfig(override, fn) {
1334
+ const run = async () => {
1335
+ const prevConfig = _config;
1336
+ const prevSession = _sessionId;
1337
+ _config = override;
1338
+ _sessionId = null;
1339
+ try {
1340
+ return await fn();
1341
+ } finally {
1342
+ _config = prevConfig;
1343
+ _sessionId = prevSession;
1344
+ }
1345
+ };
1346
+ const result = _overrideLock.then(run, run);
1347
+ _overrideLock = result.then(() => void 0, () => void 0);
1348
+ return result;
1349
+ }
1302
1350
  function parseSSE(text2) {
1303
1351
  const lines = text2.split("\n");
1304
1352
  for (const line of lines) {
@@ -1373,7 +1421,13 @@ async function callTool(toolName, args) {
1373
1421
  }
1374
1422
  const textContent = result.result?.content?.[0]?.text;
1375
1423
  if (!textContent) return null;
1376
- 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
+ }
1377
1431
  }
1378
1432
  async function writeDeck(input) {
1379
1433
  const result = await callTool("communication_write", {
@@ -1382,6 +1436,7 @@ async function writeDeck(input) {
1382
1436
  resource_type: "deck",
1383
1437
  fields: {
1384
1438
  deckHtml: input.html,
1439
+ deckBlocks: input.blocks,
1385
1440
  sessionId: input.sessionId,
1386
1441
  blockCount: input.blockCount,
1387
1442
  sharedFrom: "launch-deck"
@@ -1396,6 +1451,7 @@ async function updateDeck(commentId, input) {
1396
1451
  body: input.body,
1397
1452
  fields: {
1398
1453
  deckHtml: input.html,
1454
+ deckBlocks: input.blocks,
1399
1455
  sessionId: input.sessionId,
1400
1456
  blockCount: input.blockCount,
1401
1457
  sharedFrom: "launch-deck"
@@ -1406,16 +1462,42 @@ async function updateDeck(commentId, input) {
1406
1462
  async function deleteDeck(commentId) {
1407
1463
  await callTool("communication_delete", { comment_id: commentId });
1408
1464
  }
1409
- var import_node_fs3, import_node_path3, _config, _requestId, _sessionId;
1465
+ function currentMcpOrigin() {
1466
+ const config = _config;
1467
+ if (!config) throw new Error("MCP config not loaded \u2014 call loadMcpConfig first");
1468
+ return new URL(config.url).origin;
1469
+ }
1470
+ async function createDeckShareLink(commentId) {
1471
+ const result = await callTool("deck_share_link", {
1472
+ comment_id: commentId,
1473
+ action: "create"
1474
+ });
1475
+ const token = result.link.token;
1476
+ return {
1477
+ linkId: result.link.id,
1478
+ token,
1479
+ shareUrl: `${currentMcpOrigin()}/s/deck/${token}`,
1480
+ reused: Boolean(result.reused)
1481
+ };
1482
+ }
1483
+ async function revokeDeckShareLink(commentId, linkId) {
1484
+ await callTool("deck_share_link", {
1485
+ comment_id: commentId,
1486
+ action: "revoke",
1487
+ link_id: linkId
1488
+ });
1489
+ }
1490
+ var import_node_fs4, import_node_path4, _config, _requestId, _sessionId, _overrideLock;
1410
1491
  var init_mcp_client = __esm({
1411
1492
  "src/server/mcp-client.ts"() {
1412
1493
  "use strict";
1413
- import_node_fs3 = require("node:fs");
1414
- import_node_path3 = require("node:path");
1494
+ import_node_fs4 = require("node:fs");
1495
+ import_node_path4 = require("node:path");
1415
1496
  init_cred_shape();
1416
1497
  _config = null;
1417
1498
  _requestId = 0;
1418
1499
  _sessionId = null;
1500
+ _overrideLock = Promise.resolve();
1419
1501
  }
1420
1502
  });
1421
1503
 
@@ -1423,6 +1505,7 @@ var init_mcp_client = __esm({
1423
1505
  var deck_serve_exports = {};
1424
1506
  __export(deck_serve_exports, {
1425
1507
  broadcastToClients: () => broadcastToClients,
1508
+ buildSharePayload: () => buildSharePayload,
1426
1509
  consumeRenderError: () => consumeRenderError,
1427
1510
  createFeedbackWaiter: () => createFeedbackWaiter,
1428
1511
  resolveFeedback: () => resolveFeedback,
@@ -1430,14 +1513,14 @@ __export(deck_serve_exports, {
1430
1513
  startDeckServer: () => startDeckServer
1431
1514
  });
1432
1515
  function sessionDir(cwd, session) {
1433
- return import_node_path4.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", session);
1516
+ return import_node_path5.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", session);
1434
1517
  }
1435
1518
  function sessionJsonPath(cwd, session) {
1436
- return import_node_path4.default.join(sessionDir(cwd, session), "session.json");
1519
+ return import_node_path5.default.join(sessionDir(cwd, session), "session.json");
1437
1520
  }
1438
1521
  function readPersistedSession(cwd, session) {
1439
1522
  try {
1440
- const raw = import_node_fs4.default.readFileSync(sessionJsonPath(cwd, session), "utf-8");
1523
+ const raw = import_node_fs5.default.readFileSync(sessionJsonPath(cwd, session), "utf-8");
1441
1524
  return JSON.parse(raw);
1442
1525
  } catch {
1443
1526
  return null;
@@ -1445,7 +1528,7 @@ function readPersistedSession(cwd, session) {
1445
1528
  }
1446
1529
  function writePersistedSession(cwd, session, payload) {
1447
1530
  const dir = sessionDir(cwd, session);
1448
- import_node_fs4.default.mkdirSync(dir, { recursive: true });
1531
+ import_node_fs5.default.mkdirSync(dir, { recursive: true });
1449
1532
  const existing = readPersistedSession(cwd, session);
1450
1533
  const now = (/* @__PURE__ */ new Date()).toISOString();
1451
1534
  const next = {
@@ -1457,13 +1540,13 @@ function writePersistedSession(cwd, session, payload) {
1457
1540
  createdAt: existing?.createdAt ?? now,
1458
1541
  updatedAt: now
1459
1542
  };
1460
- import_node_fs4.default.writeFileSync(sessionJsonPath(cwd, session), JSON.stringify(next, null, 2));
1543
+ import_node_fs5.default.writeFileSync(sessionJsonPath(cwd, session), JSON.stringify(next, null, 2));
1461
1544
  return next;
1462
1545
  }
1463
1546
  function listPersistedSessions(cwd) {
1464
- const base = import_node_path4.default.join(cwd, LAUNCHSECURE_DIR, "deck-files");
1465
- if (!import_node_fs4.default.existsSync(base)) return [];
1466
- const entries = import_node_fs4.default.readdirSync(base, { withFileTypes: true });
1547
+ const base = import_node_path5.default.join(cwd, LAUNCHSECURE_DIR, "deck-files");
1548
+ if (!import_node_fs5.default.existsSync(base)) return [];
1549
+ const entries = import_node_fs5.default.readdirSync(base, { withFileTypes: true });
1467
1550
  const sessions = [];
1468
1551
  for (const entry of entries) {
1469
1552
  if (!entry.isDirectory()) continue;
@@ -1478,33 +1561,53 @@ function listPersistedSessions(cwd) {
1478
1561
  createdAt: persisted.createdAt,
1479
1562
  updatedAt: persisted.updatedAt,
1480
1563
  shared: Boolean(sync),
1481
- stale: Boolean(sync) && persisted.version > (sync?.version ?? 0)
1564
+ stale: Boolean(sync) && persisted.version > (sync?.version ?? 0),
1565
+ shareUrl: sync?.publicShareUrl,
1566
+ course: sync?.course
1482
1567
  });
1483
1568
  }
1484
1569
  sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1485
1570
  return sessions;
1486
1571
  }
1487
1572
  function deletePersistedSession(cwd, session) {
1488
- const base = import_node_path4.default.join(cwd, LAUNCHSECURE_DIR, "deck-files");
1573
+ const base = import_node_path5.default.join(cwd, LAUNCHSECURE_DIR, "deck-files");
1489
1574
  if (session === "all") {
1490
- if (import_node_fs4.default.existsSync(base)) import_node_fs4.default.rmSync(base, { recursive: true, force: true });
1575
+ if (import_node_fs5.default.existsSync(base)) import_node_fs5.default.rmSync(base, { recursive: true, force: true });
1491
1576
  return;
1492
1577
  }
1493
1578
  const dir = sessionDir(cwd, session);
1494
- if (import_node_fs4.default.existsSync(dir)) import_node_fs4.default.rmSync(dir, { recursive: true, force: true });
1579
+ if (import_node_fs5.default.existsSync(dir)) import_node_fs5.default.rmSync(dir, { recursive: true, force: true });
1495
1580
  }
1496
1581
  function syncJsonPath(cwd, session) {
1497
- return import_node_path4.default.join(sessionDir(cwd, session), ".sync.json");
1582
+ return import_node_path5.default.join(sessionDir(cwd, session), ".sync.json");
1498
1583
  }
1499
1584
  function readSyncRecord(cwd, session) {
1500
1585
  try {
1501
- return JSON.parse(import_node_fs4.default.readFileSync(syncJsonPath(cwd, session), "utf-8"));
1586
+ return JSON.parse(import_node_fs5.default.readFileSync(syncJsonPath(cwd, session), "utf-8"));
1502
1587
  } catch {
1503
1588
  return null;
1504
1589
  }
1505
1590
  }
1506
1591
  function writeSyncRecord(cwd, session, rec) {
1507
- import_node_fs4.default.writeFileSync(syncJsonPath(cwd, session), JSON.stringify(rec, null, 2));
1592
+ import_node_fs5.default.writeFileSync(syncJsonPath(cwd, session), JSON.stringify(rec, null, 2));
1593
+ }
1594
+ function resolveShareConfig(cwd, course) {
1595
+ if (course) {
1596
+ const override = mcpConfigForProfile(cwd, course);
1597
+ if (!override) {
1598
+ return { error: `Course "${course}" is not a usable profile in .launch-secure.cred.config (missing pat/serverUrl/orgSlug/projectSlug?).` };
1599
+ }
1600
+ return { override };
1601
+ }
1602
+ try {
1603
+ loadMcpConfig(cwd);
1604
+ } catch (err) {
1605
+ return { error: `Cloud sharing needs an active LaunchSecure course (.launch-secure.cred.config) or a "launch-secure" entry in .mcp.json: ${String(err)}` };
1606
+ }
1607
+ return { override: null };
1608
+ }
1609
+ function runWithShareConfig(override, fn) {
1610
+ return override ? withMcpConfig(override, fn) : fn();
1508
1611
  }
1509
1612
  function escHtml2(s) {
1510
1613
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -1517,36 +1620,54 @@ function readIframeArtifact(cwd, src) {
1517
1620
  const rel = decodeURIComponent(src.slice("/deck-files/".length));
1518
1621
  if (rel.includes("..")) return null;
1519
1622
  try {
1520
- return import_node_fs4.default.readFileSync(import_node_path4.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", rel), "utf-8");
1623
+ return import_node_fs5.default.readFileSync(import_node_path5.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", rel), "utf-8");
1521
1624
  } catch {
1522
1625
  return null;
1523
1626
  }
1524
1627
  }
1525
- function renderShareSection(cwd, block) {
1526
- const label = block?.label ? `<h2 class="db-title">${escHtml2(String(block.label))}</h2>` : "";
1628
+ function renderShareSection(cwd, block, showLabel = true) {
1629
+ const label = showLabel && block?.label ? `<h2 class="db-title">${escHtml2(String(block.label))}</h2>` : "";
1527
1630
  switch (block?.type) {
1528
1631
  case "iframe": {
1529
1632
  const doc = readIframeArtifact(cwd, block.src) ?? "<p>(missing artifact)</p>";
1530
- return `<section class="db">${label}<iframe class="db-frame" sandbox="allow-scripts allow-popups" srcdoc="${escAttr(doc)}"></iframe></section>`;
1633
+ return `${label}<iframe class="db-frame" sandbox="allow-scripts allow-popups" srcdoc="${escAttr(doc)}"></iframe>`;
1531
1634
  }
1532
1635
  case "rich-html":
1533
1636
  case "html":
1534
- return `<section class="db">${label}<div class="db-html">${typeof block.content === "string" ? block.content : ""}</div></section>`;
1637
+ return `${label}<div class="db-html">${typeof block.content === "string" ? block.content : ""}</div>`;
1535
1638
  case "markdown":
1536
- return `<section class="db">${label}<div class="db-md" data-md="${escAttr(typeof block.content === "string" ? block.content : "")}"></div></section>`;
1639
+ return `${label}<div class="db-md" data-md="${escAttr(typeof block.content === "string" ? block.content : "")}"></div>`;
1537
1640
  case "mermaid":
1538
- return `<section class="db">${label}<pre class="mermaid">${escHtml2(typeof block.content === "string" ? block.content : "")}</pre></section>`;
1641
+ if (typeof block.renderedSvg === "string" && block.renderedSvg.trim()) {
1642
+ return `${label}<div class="mermaid-svg">${block.renderedSvg}</div>`;
1643
+ }
1644
+ return `${label}<pre class="mermaid">${escHtml2(typeof block.content === "string" ? block.content : "")}</pre>`;
1539
1645
  case "options": {
1540
1646
  const opts = Array.isArray(block.options) ? block.options : [];
1541
- const lis = opts.map(
1542
- (o) => `<li><strong>${escHtml2(String(o?.label ?? o?.id ?? ""))}</strong>${o?.description ? ` \u2014 ${escHtml2(String(o.description))}` : ""}</li>`
1647
+ const cards = opts.map(
1648
+ (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>`
1543
1649
  ).join("");
1544
- return `<section class="db">${label}<ul>${lis}</ul></section>`;
1650
+ return `${label}<div class="db-options">${cards}</div>`;
1545
1651
  }
1546
1652
  default:
1547
- return `<section class="db">${label}</section>`;
1653
+ return label;
1548
1654
  }
1549
1655
  }
1656
+ function buildSharePayload(cwd, session, blocks, svgByIndex = {}) {
1657
+ const enriched = blocks.map((b, i) => {
1658
+ const svg = svgByIndex[String(i)];
1659
+ return b?.type === "mermaid" && typeof svg === "string" && svg.trim() ? { ...b, renderedSvg: svg } : b;
1660
+ });
1661
+ const html = buildShareHtml(cwd, session, enriched);
1662
+ const cloudBlocks = enriched.map((b) => {
1663
+ if (b?.type === "iframe" && typeof b.src === "string") {
1664
+ const content = readIframeArtifact(cwd, b.src);
1665
+ return content ? { ...b, content } : b;
1666
+ }
1667
+ return b;
1668
+ });
1669
+ return { html, cloudBlocks };
1670
+ }
1550
1671
  function buildShareHtml(cwd, session, blocks) {
1551
1672
  if (blocks.length === 1) {
1552
1673
  const only = blocks[0];
@@ -1556,28 +1677,57 @@ function buildShareHtml(cwd, session, blocks) {
1556
1677
  }
1557
1678
  }
1558
1679
  const list = blocks;
1559
- const sections = list.map((b) => renderShareSection(cwd, b)).join("\n");
1680
+ const multi = list.length > 1;
1681
+ const tabBar = multi ? `<nav class="deck-tabs">${list.map(
1682
+ (b, i) => `<button class="deck-tab${i === 0 ? " active" : ""}" data-tab-btn="${i}">${escHtml2(String(b?.label ?? `Tab ${i + 1}`))}</button>`
1683
+ ).join("")}</nav>` : "";
1684
+ const sections = list.map(
1685
+ (b, i) => `<section class="db" data-tab="${i}"${multi && i > 0 ? " hidden" : ""}>${renderShareSection(cwd, b, !multi)}</section>`
1686
+ ).join("\n");
1560
1687
  const hasMd = list.some((b) => b?.type === "markdown");
1561
- const hasMermaid = list.some((b) => b?.type === "mermaid");
1688
+ const hasUnrenderedMermaid = list.some(
1689
+ (b) => b?.type === "mermaid" && !(typeof b?.renderedSvg === "string" && b.renderedSvg.trim())
1690
+ );
1562
1691
  return `<!DOCTYPE html>
1563
1692
  <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
1564
1693
  <title>${escHtml2(session)}</title>
1694
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,100..1000&display=swap" rel="stylesheet">
1565
1695
  <style>
1566
- body { margin:0; font-family: ui-sans-serif, system-ui, sans-serif; background:#0b0e14; color:#e6e9ef; }
1696
+ body { margin:0; font-family: 'DM Sans', system-ui, sans-serif; background:#0b0e14; color:#e6e9ef; }
1567
1697
  .db { padding:20px 24px; border-bottom:1px solid #1e2433; }
1568
1698
  .db-title { font-size:13px; font-weight:600; margin:0 0 12px; color:#9aa4b2; text-transform:uppercase; letter-spacing:.05em; }
1569
1699
  .db-frame { width:100%; height:70vh; border:0; border-radius:8px; background:#fff; }
1570
1700
  .db-html { background:#fff; color:#111; border-radius:8px; padding:16px; overflow:auto; }
1571
1701
  ul { line-height:1.7; }
1572
- .mermaid { background:#fff; border-radius:8px; padding:16px; }
1702
+ /* The embedded SVG is dark-themed (captured from the deck client); keep its
1703
+ backdrop transparent so it sits on the dark page, matching the in-app
1704
+ DeckView \u2014 not a white card. The CDN-source fallback also renders dark. */
1705
+ .mermaid { background:transparent; border-radius:8px; padding:16px; }
1706
+ .mermaid-svg { background:transparent; border-radius:8px; padding:16px; overflow:auto; }
1707
+ .mermaid-svg svg { max-width:100%; height:auto; }
1708
+ .db-options { display:grid; gap:12px; grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); }
1709
+ .db-option { border:2px solid #1e2433; border-radius:8px; padding:16px; }
1710
+ .db-option-label { font-size:14px; font-weight:600; color:#e6e9ef; }
1711
+ .db-option-desc { margin-top:4px; font-size:12px; color:#9aa4b2; }
1712
+ .db-option-pre { margin-top:8px; padding:8px; border-radius:6px; background:#11151f; font-size:12px; overflow:auto; }
1713
+ .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; }
1714
+ .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; }
1715
+ .deck-tab:hover { color:#e6e9ef; }
1716
+ .deck-tab.active { color:#c084fc; border-bottom-color:#c084fc; }
1717
+ .db[hidden] { display:none; }
1573
1718
  </style></head>
1574
1719
  <body>
1720
+ ${tabBar}
1575
1721
  ${sections}
1722
+ ${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>` : ""}
1576
1723
  ${hasMd ? `<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
1577
1724
  <script>document.querySelectorAll('.db-md').forEach(function(el){try{el.innerHTML=marked.parse(el.dataset.md||'')}catch(e){el.textContent=el.dataset.md||''}});</script>` : ""}
1578
- ${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>` : ""}
1725
+ ${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>` : ""}
1579
1726
  </body></html>`;
1580
1727
  }
1728
+ function broadcastDiscuss() {
1729
+ broadcastToClients({ type: "discuss_session", sessions: discussSessions.map((d) => d.id) });
1730
+ }
1581
1731
  function consumeRenderError() {
1582
1732
  const err = lastRenderError;
1583
1733
  lastRenderError = null;
@@ -1614,23 +1764,23 @@ function broadcastToClients(message) {
1614
1764
  }
1615
1765
  }
1616
1766
  function serveStatic(res, filePath) {
1617
- if (!import_node_fs4.default.existsSync(filePath) || !import_node_fs4.default.statSync(filePath).isFile()) return false;
1618
- const ext = import_node_path4.default.extname(filePath).toLowerCase();
1767
+ if (!import_node_fs5.default.existsSync(filePath) || !import_node_fs5.default.statSync(filePath).isFile()) return false;
1768
+ const ext = import_node_path5.default.extname(filePath).toLowerCase();
1619
1769
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
1620
1770
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
1621
- import_node_fs4.default.createReadStream(filePath).pipe(res);
1771
+ import_node_fs5.default.createReadStream(filePath).pipe(res);
1622
1772
  return true;
1623
1773
  }
1624
1774
  function resolveClientDir() {
1625
- const sibling = import_node_path4.default.join(__dirname, "..", "deck-client");
1626
- if (import_node_fs4.default.existsSync(import_node_path4.default.join(sibling, "assets"))) return sibling;
1627
- const devDist = import_node_path4.default.join(__dirname, "..", "..", "dist", "deck-client");
1628
- if (import_node_fs4.default.existsSync(import_node_path4.default.join(devDist, "assets"))) return devDist;
1775
+ const sibling = import_node_path5.default.join(__dirname, "..", "deck-client");
1776
+ if (import_node_fs5.default.existsSync(import_node_path5.default.join(sibling, "assets"))) return sibling;
1777
+ const devDist = import_node_path5.default.join(__dirname, "..", "..", "dist", "deck-client");
1778
+ if (import_node_fs5.default.existsSync(import_node_path5.default.join(devDist, "assets"))) return devDist;
1629
1779
  return sibling;
1630
1780
  }
1631
1781
  function serveIndex(res, clientDir) {
1632
- const indexPath = import_node_path4.default.join(clientDir, "index.html");
1633
- if (!import_node_fs4.default.existsSync(indexPath)) {
1782
+ const indexPath = import_node_path5.default.join(clientDir, "index.html");
1783
+ if (!import_node_fs5.default.existsSync(indexPath)) {
1634
1784
  res.writeHead(500, { "Content-Type": "text/plain" });
1635
1785
  res.end(`LaunchDeck client bundle not found at ${clientDir}. Run 'npm run build:client'.`);
1636
1786
  return;
@@ -1713,6 +1863,10 @@ async function startDeckServer(opts = {}) {
1713
1863
  jsonResponse(res, 200, { config: cfg });
1714
1864
  return;
1715
1865
  }
1866
+ if (req.method === "GET" && url2.pathname === "/api/deck/courses") {
1867
+ jsonResponse(res, 200, { courses: listCourses(cwd) });
1868
+ return;
1869
+ }
1716
1870
  if (req.method === "POST" && url2.pathname === "/api/deck/config") {
1717
1871
  try {
1718
1872
  const body = JSON.parse(await readBody(req));
@@ -1734,15 +1888,15 @@ async function startDeckServer(opts = {}) {
1734
1888
  const baseUrl = `/deck-files/${sessionEncoded}`;
1735
1889
  const html = generateBlastRadiusHtml(block.manifest, baseUrl);
1736
1890
  const report = generateBlastRadiusReport(block.manifest);
1737
- const dir = import_node_path4.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", body.session);
1738
- import_node_fs4.default.mkdirSync(dir, { recursive: true });
1739
- import_node_fs4.default.writeFileSync(import_node_path4.default.join(dir, "blast-radius.html"), html);
1740
- import_node_fs4.default.writeFileSync(import_node_path4.default.join(dir, "blast-radius-report.md"), report);
1741
- import_node_fs4.default.writeFileSync(import_node_path4.default.join(dir, "blast-radius-manifest.json"), JSON.stringify(block.manifest, null, 2));
1891
+ const dir = import_node_path5.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", body.session);
1892
+ import_node_fs5.default.mkdirSync(dir, { recursive: true });
1893
+ import_node_fs5.default.writeFileSync(import_node_path5.default.join(dir, "blast-radius.html"), html);
1894
+ import_node_fs5.default.writeFileSync(import_node_path5.default.join(dir, "blast-radius-report.md"), report);
1895
+ import_node_fs5.default.writeFileSync(import_node_path5.default.join(dir, "blast-radius-manifest.json"), JSON.stringify(block.manifest, null, 2));
1742
1896
  const contractData = generateContract(block.manifest);
1743
1897
  const contractMd = contractToMarkdown(contractData);
1744
- import_node_fs4.default.writeFileSync(import_node_path4.default.join(dir, "contract.json"), JSON.stringify(contractData, null, 2));
1745
- import_node_fs4.default.writeFileSync(import_node_path4.default.join(dir, "contract.md"), contractMd);
1898
+ import_node_fs5.default.writeFileSync(import_node_path5.default.join(dir, "contract.json"), JSON.stringify(contractData, null, 2));
1899
+ import_node_fs5.default.writeFileSync(import_node_path5.default.join(dir, "contract.md"), contractMd);
1746
1900
  block.type = "iframe";
1747
1901
  block.src = `${baseUrl}/blast-radius.html`;
1748
1902
  delete block.manifest;
@@ -1769,9 +1923,9 @@ async function startDeckServer(opts = {}) {
1769
1923
  const sessionEncoded = encodeURIComponent(body.session);
1770
1924
  const baseUrl = `/deck-files/${sessionEncoded}`;
1771
1925
  const filename = `${slugify(block.label)}.html`;
1772
- const dir = import_node_path4.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", body.session);
1773
- import_node_fs4.default.mkdirSync(dir, { recursive: true });
1774
- import_node_fs4.default.writeFileSync(import_node_path4.default.join(dir, filename), html);
1926
+ const dir = import_node_path5.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", body.session);
1927
+ import_node_fs5.default.mkdirSync(dir, { recursive: true });
1928
+ import_node_fs5.default.writeFileSync(import_node_path5.default.join(dir, filename), html);
1775
1929
  block.type = "iframe";
1776
1930
  block.src = `${baseUrl}/${filename}`;
1777
1931
  delete block.content;
@@ -1811,40 +1965,128 @@ async function startDeckServer(opts = {}) {
1811
1965
  jsonResponse(res, 404, { ok: false, error: "session not found" });
1812
1966
  return;
1813
1967
  }
1968
+ let course;
1969
+ let svgByIndex = {};
1970
+ let publicLink = false;
1814
1971
  try {
1815
- loadMcpConfig(cwd);
1816
- } catch (err) {
1817
- jsonResponse(res, 400, {
1818
- ok: false,
1819
- error: `Cloud sharing needs an active LaunchSecure course (.launch-secure.cred.config) or a "launch-secure" entry in .mcp.json: ${String(err)}`
1820
- });
1972
+ const raw = await readBody(req);
1973
+ if (raw.trim()) {
1974
+ const parsed = JSON.parse(raw);
1975
+ course = parsed.course?.trim() || void 0;
1976
+ if (parsed.svgByIndex && typeof parsed.svgByIndex === "object") svgByIndex = parsed.svgByIndex;
1977
+ publicLink = parsed.publicLink === true;
1978
+ }
1979
+ } catch {
1980
+ }
1981
+ const cfg = resolveShareConfig(cwd, course);
1982
+ if ("error" in cfg) {
1983
+ jsonResponse(res, 400, { ok: false, error: cfg.error });
1821
1984
  return;
1822
1985
  }
1986
+ const { override } = cfg;
1823
1987
  try {
1824
1988
  const blocks = Array.isArray(persisted.blocks) ? persisted.blocks : [];
1825
- const html = buildShareHtml(cwd, id, blocks);
1989
+ const { html, cloudBlocks } = buildSharePayload(cwd, id, blocks, svgByIndex);
1826
1990
  const types = blocks.map((b) => b?.type).filter(Boolean);
1827
1991
  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.`;
1828
- const input = { title: id, body, html, sessionId: id, blockCount: blocks.length };
1829
- const existing2 = readSyncRecord(cwd, id);
1830
- let resourceId;
1831
- let updated = false;
1832
- if (existing2?.resourceId) {
1992
+ const input = { title: id, body, html, blocks: cloudBlocks, sessionId: id, blockCount: blocks.length };
1993
+ const existingSync = readSyncRecord(cwd, id);
1994
+ const doShare = async () => {
1995
+ if (existingSync?.resourceId) {
1996
+ try {
1997
+ const { id: rid2 } = await updateDeck(existingSync.resourceId, input);
1998
+ return { resourceId: rid2, updated: true };
1999
+ } catch {
2000
+ const { id: rid2 } = await writeDeck(input);
2001
+ return { resourceId: rid2, updated: false };
2002
+ }
2003
+ }
2004
+ const { id: rid } = await writeDeck(input);
2005
+ return { resourceId: rid, updated: false };
2006
+ };
2007
+ const result = await runWithShareConfig(override, async () => {
2008
+ const shared = await doShare();
2009
+ let link = null;
2010
+ let linkError;
2011
+ if (publicLink) {
2012
+ try {
2013
+ const minted = await createDeckShareLink(shared.resourceId);
2014
+ link = { shareUrl: minted.shareUrl, linkId: minted.linkId };
2015
+ } catch (err) {
2016
+ linkError = err instanceof Error ? err.message : String(err);
2017
+ }
2018
+ }
2019
+ return { shared, link, linkError };
2020
+ });
2021
+ if (publicLink && result.linkError && !result.shared.updated) {
1833
2022
  try {
1834
- ({ id: resourceId } = await updateDeck(existing2.resourceId, input));
1835
- updated = true;
2023
+ await runWithShareConfig(override, () => deleteDeck(result.shared.resourceId));
1836
2024
  } catch {
1837
- ({ id: resourceId } = await writeDeck(input));
1838
2025
  }
1839
- } else {
1840
- ({ id: resourceId } = await writeDeck(input));
2026
+ jsonResponse(res, 200, {
2027
+ ok: false,
2028
+ error: `Couldn't create the public link, so the deck wasn't shared: ${result.linkError}`
2029
+ });
2030
+ return;
1841
2031
  }
2032
+ const publicShareUrl = result.link?.shareUrl ?? existingSync?.publicShareUrl;
2033
+ const publicLinkId = result.link?.linkId ?? existingSync?.publicLinkId;
1842
2034
  writeSyncRecord(cwd, id, {
1843
- resourceId,
2035
+ resourceId: result.shared.resourceId,
1844
2036
  sharedAt: (/* @__PURE__ */ new Date()).toISOString(),
1845
- version: persisted.version
2037
+ version: persisted.version,
2038
+ course,
2039
+ ...publicShareUrl ? { publicShareUrl } : {},
2040
+ ...publicLinkId ? { publicLinkId } : {}
2041
+ });
2042
+ jsonResponse(res, 200, {
2043
+ ok: true,
2044
+ resourceId: result.shared.resourceId,
2045
+ updated: result.shared.updated,
2046
+ course,
2047
+ shareUrl: publicShareUrl ?? null,
2048
+ ...result.linkError ? { linkWarning: result.linkError } : {}
1846
2049
  });
1847
- jsonResponse(res, 200, { ok: true, resourceId, updated });
2050
+ } catch (err) {
2051
+ jsonResponse(res, 500, { ok: false, error: String(err) });
2052
+ }
2053
+ return;
2054
+ }
2055
+ const publicLinkMatch = url2.pathname.match(/^\/api\/sessions\/([^/]+)\/public-link$/);
2056
+ if (publicLinkMatch && (req.method === "POST" || req.method === "DELETE")) {
2057
+ const id = decodeURIComponent(publicLinkMatch[1]);
2058
+ const sync = readSyncRecord(cwd, id);
2059
+ if (!sync?.resourceId) {
2060
+ jsonResponse(res, 409, { ok: false, error: "Share this deck to the cloud first, then create a public link." });
2061
+ return;
2062
+ }
2063
+ const cfg = resolveShareConfig(cwd, sync.course);
2064
+ if ("error" in cfg) {
2065
+ jsonResponse(res, 400, { ok: false, error: cfg.error });
2066
+ return;
2067
+ }
2068
+ const { override } = cfg;
2069
+ if (req.method === "DELETE") {
2070
+ if (!sync.publicLinkId) {
2071
+ if (sync.publicShareUrl) {
2072
+ writeSyncRecord(cwd, id, { ...sync, publicShareUrl: void 0, publicLinkId: void 0 });
2073
+ }
2074
+ jsonResponse(res, 200, { ok: true, revoked: false });
2075
+ return;
2076
+ }
2077
+ try {
2078
+ await runWithShareConfig(override, () => revokeDeckShareLink(sync.resourceId, sync.publicLinkId));
2079
+ writeSyncRecord(cwd, id, { ...sync, publicShareUrl: void 0, publicLinkId: void 0 });
2080
+ jsonResponse(res, 200, { ok: true, revoked: true });
2081
+ } catch (err) {
2082
+ jsonResponse(res, 500, { ok: false, error: String(err) });
2083
+ }
2084
+ return;
2085
+ }
2086
+ try {
2087
+ const minted = await runWithShareConfig(override, () => createDeckShareLink(sync.resourceId));
2088
+ writeSyncRecord(cwd, id, { ...sync, publicShareUrl: minted.shareUrl, publicLinkId: minted.linkId });
2089
+ jsonResponse(res, 200, { ok: true, shareUrl: minted.shareUrl });
1848
2090
  } catch (err) {
1849
2091
  jsonResponse(res, 500, { ok: false, error: String(err) });
1850
2092
  }
@@ -1867,12 +2109,21 @@ async function startDeckServer(opts = {}) {
1867
2109
  try {
1868
2110
  await deleteDeck(sync.resourceId);
1869
2111
  try {
1870
- import_node_fs4.default.rmSync(syncJsonPath(cwd, id), { force: true });
2112
+ import_node_fs5.default.rmSync(syncJsonPath(cwd, id), { force: true });
1871
2113
  } catch {
1872
2114
  }
1873
2115
  jsonResponse(res, 200, { ok: true, deleted: true });
1874
2116
  } catch (err) {
1875
- jsonResponse(res, 500, { ok: false, error: String(err) });
2117
+ const msg = err instanceof Error ? err.message : String(err);
2118
+ if (/not\s*found/i.test(msg)) {
2119
+ try {
2120
+ import_node_fs5.default.rmSync(syncJsonPath(cwd, id), { force: true });
2121
+ } catch {
2122
+ }
2123
+ jsonResponse(res, 200, { ok: true, deleted: false, reason: "already removed" });
2124
+ return;
2125
+ }
2126
+ jsonResponse(res, 500, { ok: false, error: msg });
1876
2127
  }
1877
2128
  return;
1878
2129
  }
@@ -1963,6 +2214,10 @@ async function startDeckServer(opts = {}) {
1963
2214
  deletePersistedSession(cwd, session);
1964
2215
  } catch {
1965
2216
  }
2217
+ const beforePins = discussSessions.length;
2218
+ discussSessions = session === "all" ? [] : discussSessions.filter((d) => d.id !== session);
2219
+ if (discussSessions.length !== beforePins) broadcastDiscuss();
2220
+ if (session === "all" || activeSession?.id === session) activeSession = null;
1966
2221
  jsonResponse(res, 200, { ok: true });
1967
2222
  return;
1968
2223
  }
@@ -1971,6 +2226,16 @@ async function startDeckServer(opts = {}) {
1971
2226
  jsonResponse(res, 200, err ?? null);
1972
2227
  return;
1973
2228
  }
2229
+ if (req.method === "GET" && url2.pathname === "/api/active-session") {
2230
+ jsonResponse(res, 200, {
2231
+ discuss: discussSessions.map((d) => d.id),
2232
+ // ordered = pin number
2233
+ active: activeSession?.id ?? null,
2234
+ focused: activeSession?.focused ?? false,
2235
+ activeAt: activeSession?.at ?? null
2236
+ });
2237
+ return;
2238
+ }
1974
2239
  if (req.method === "GET" && url2.pathname.startsWith("/deck-files/")) {
1975
2240
  const relative = decodeURIComponent(url2.pathname.slice("/deck-files/".length));
1976
2241
  if (relative.includes("..") || relative.startsWith("/")) {
@@ -1978,7 +2243,7 @@ async function startDeckServer(opts = {}) {
1978
2243
  res.end("Forbidden");
1979
2244
  return;
1980
2245
  }
1981
- const filePath = import_node_path4.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", relative);
2246
+ const filePath = import_node_path5.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", relative);
1982
2247
  if (serveStatic(res, filePath)) return;
1983
2248
  res.writeHead(404);
1984
2249
  res.end("Not found");
@@ -1991,25 +2256,25 @@ async function startDeckServer(opts = {}) {
1991
2256
  res.end("Forbidden");
1992
2257
  return;
1993
2258
  }
1994
- const filePath = import_node_path4.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", relative);
1995
- if (!import_node_fs4.default.existsSync(filePath) || !import_node_fs4.default.statSync(filePath).isFile()) {
2259
+ const filePath = import_node_path5.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", relative);
2260
+ if (!import_node_fs5.default.existsSync(filePath) || !import_node_fs5.default.statSync(filePath).isFile()) {
1996
2261
  res.writeHead(404);
1997
2262
  res.end("Not found");
1998
2263
  return;
1999
2264
  }
2000
- const ext = import_node_path4.default.extname(filePath).toLowerCase();
2265
+ const ext = import_node_path5.default.extname(filePath).toLowerCase();
2001
2266
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
2002
- const filename = import_node_path4.default.basename(filePath);
2267
+ const filename = import_node_path5.default.basename(filePath);
2003
2268
  res.writeHead(200, {
2004
2269
  "Content-Type": mime,
2005
2270
  "Content-Disposition": `attachment; filename="${filename}"`,
2006
2271
  "Cache-Control": "no-cache"
2007
2272
  });
2008
- import_node_fs4.default.createReadStream(filePath).pipe(res);
2273
+ import_node_fs5.default.createReadStream(filePath).pipe(res);
2009
2274
  return;
2010
2275
  }
2011
2276
  if (url2.pathname !== "/") {
2012
- const staticPath = import_node_path4.default.join(clientDir, url2.pathname);
2277
+ const staticPath = import_node_path5.default.join(clientDir, url2.pathname);
2013
2278
  if (serveStatic(res, staticPath)) return;
2014
2279
  }
2015
2280
  serveIndex(res, clientDir);
@@ -2043,6 +2308,24 @@ async function startDeckServer(opts = {}) {
2043
2308
  source: msg.source ?? ""
2044
2309
  };
2045
2310
  }
2311
+ if (msg.type === "active_session" && msg.session) {
2312
+ activeSession = {
2313
+ id: msg.session,
2314
+ focused: msg.focused ?? false,
2315
+ at: Date.now()
2316
+ };
2317
+ }
2318
+ if (msg.type === "discuss_session" && msg.session) {
2319
+ const id = msg.session;
2320
+ const idx = discussSessions.findIndex((d) => d.id === id);
2321
+ if (idx >= 0) discussSessions.splice(idx, 1);
2322
+ else discussSessions.push({ id, at: Date.now() });
2323
+ broadcastDiscuss();
2324
+ }
2325
+ if (msg.type === "discuss_clear_all") {
2326
+ discussSessions = [];
2327
+ broadcastDiscuss();
2328
+ }
2046
2329
  } catch {
2047
2330
  }
2048
2331
  });
@@ -2108,13 +2391,13 @@ function runServeCli(argv) {
2108
2391
  process.exit(1);
2109
2392
  });
2110
2393
  }
2111
- var import_node_http, import_node_fs4, import_node_path4, import_ws, DEFAULT_PORT, MAX_PORT_SCAN, MIME_TYPES, pendingFeedback, lastRenderError, wss;
2394
+ var import_node_http, import_node_fs5, import_node_path5, import_ws, DEFAULT_PORT, MAX_PORT_SCAN, MIME_TYPES, pendingFeedback, lastRenderError, activeSession, discussSessions, wss;
2112
2395
  var init_deck_serve = __esm({
2113
2396
  "src/server/deck-serve.ts"() {
2114
2397
  "use strict";
2115
2398
  import_node_http = __toESM(require("node:http"));
2116
- import_node_fs4 = __toESM(require("node:fs"));
2117
- import_node_path4 = __toESM(require("node:path"));
2399
+ import_node_fs5 = __toESM(require("node:fs"));
2400
+ import_node_path5 = __toESM(require("node:path"));
2118
2401
  import_ws = require("ws");
2119
2402
  init_launch_kit_paths();
2120
2403
  init_deck_lockfile();
@@ -2138,6 +2421,8 @@ var init_deck_serve = __esm({
2138
2421
  };
2139
2422
  pendingFeedback = /* @__PURE__ */ new Map();
2140
2423
  lastRenderError = null;
2424
+ activeSession = null;
2425
+ discussSessions = [];
2141
2426
  wss = null;
2142
2427
  }
2143
2428
  });
@@ -2208,15 +2493,15 @@ async function handleTool(name, args) {
2208
2493
  const baseUrl = `/deck-files/${sessionEncoded}`;
2209
2494
  const html = generateBlastRadiusHtml(block.manifest, baseUrl);
2210
2495
  const report = generateBlastRadiusReport(block.manifest);
2211
- const dir = (0, import_node_path5.join)(projectRoot, LAUNCHSECURE_DIR, "deck-files", session);
2212
- import_node_fs5.default.mkdirSync(dir, { recursive: true });
2213
- import_node_fs5.default.writeFileSync((0, import_node_path5.join)(dir, "blast-radius.html"), html);
2214
- import_node_fs5.default.writeFileSync((0, import_node_path5.join)(dir, "blast-radius-report.md"), report);
2215
- import_node_fs5.default.writeFileSync((0, import_node_path5.join)(dir, "blast-radius-manifest.json"), JSON.stringify(block.manifest, null, 2));
2496
+ const dir = (0, import_node_path6.join)(projectRoot, LAUNCHSECURE_DIR, "deck-files", session);
2497
+ import_node_fs6.default.mkdirSync(dir, { recursive: true });
2498
+ import_node_fs6.default.writeFileSync((0, import_node_path6.join)(dir, "blast-radius.html"), html);
2499
+ import_node_fs6.default.writeFileSync((0, import_node_path6.join)(dir, "blast-radius-report.md"), report);
2500
+ import_node_fs6.default.writeFileSync((0, import_node_path6.join)(dir, "blast-radius-manifest.json"), JSON.stringify(block.manifest, null, 2));
2216
2501
  const contractData = generateContract(block.manifest);
2217
2502
  const contractMd = contractToMarkdown(contractData);
2218
- import_node_fs5.default.writeFileSync((0, import_node_path5.join)(dir, "contract.json"), JSON.stringify(contractData, null, 2));
2219
- import_node_fs5.default.writeFileSync((0, import_node_path5.join)(dir, "contract.md"), contractMd);
2503
+ import_node_fs6.default.writeFileSync((0, import_node_path6.join)(dir, "contract.json"), JSON.stringify(contractData, null, 2));
2504
+ import_node_fs6.default.writeFileSync((0, import_node_path6.join)(dir, "contract.md"), contractMd);
2220
2505
  block.type = "iframe";
2221
2506
  block.src = `${baseUrl}/blast-radius.html`;
2222
2507
  delete block.manifest;
@@ -2243,9 +2528,9 @@ async function handleTool(name, args) {
2243
2528
  const sessionEncoded = encodeURIComponent(session);
2244
2529
  const baseUrl = `/deck-files/${sessionEncoded}`;
2245
2530
  const filename = `${slugify(block.label)}.html`;
2246
- const dir = (0, import_node_path5.join)(projectRoot, LAUNCHSECURE_DIR, "deck-files", session);
2247
- import_node_fs5.default.mkdirSync(dir, { recursive: true });
2248
- import_node_fs5.default.writeFileSync((0, import_node_path5.join)(dir, filename), html);
2531
+ const dir = (0, import_node_path6.join)(projectRoot, LAUNCHSECURE_DIR, "deck-files", session);
2532
+ import_node_fs6.default.mkdirSync(dir, { recursive: true });
2533
+ import_node_fs6.default.writeFileSync((0, import_node_path6.join)(dir, filename), html);
2249
2534
  block.type = "iframe";
2250
2535
  block.src = `${baseUrl}/${filename}`;
2251
2536
  delete block.content;
@@ -2339,11 +2624,11 @@ async function handleTool(name, args) {
2339
2624
  }));
2340
2625
  }
2341
2626
  try {
2342
- const logDir = (0, import_node_path5.join)((0, import_node_os2.homedir)(), LAUNCHSECURE_DIR);
2343
- (0, import_node_fs6.mkdirSync)(logDir, { recursive: true });
2344
- const logPath = (0, import_node_path5.join)(logDir, "launch-deck.log");
2345
- const out = (0, import_node_fs6.openSync)(logPath, "a");
2346
- const err = (0, import_node_fs6.openSync)(logPath, "a");
2627
+ const logDir = (0, import_node_path6.join)((0, import_node_os3.homedir)(), LAUNCHSECURE_DIR);
2628
+ (0, import_node_fs7.mkdirSync)(logDir, { recursive: true });
2629
+ const logPath = (0, import_node_path6.join)(logDir, "launch-deck.log");
2630
+ const out = (0, import_node_fs7.openSync)(logPath, "a");
2631
+ const err = (0, import_node_fs7.openSync)(logPath, "a");
2347
2632
  const entryPath = process.argv[1];
2348
2633
  const config = loadDeckConfig(projectRoot);
2349
2634
  const resolvedPort = args.port ?? config.port;
@@ -2397,23 +2682,47 @@ async function handleTool(name, args) {
2397
2682
  }
2398
2683
  return text(JSON.stringify({ running: false }));
2399
2684
  }
2685
+ case "active_session": {
2686
+ const lock = getLiveLock(projectRoot);
2687
+ if (!lock) {
2688
+ return text(JSON.stringify({
2689
+ error: "LaunchDeck server is not running \u2014 nothing is open.",
2690
+ hint: "Start it with start_server, or ask the user to open the deck."
2691
+ }));
2692
+ }
2693
+ try {
2694
+ const resp = await fetch(`${lock.url}/api/active-session`, {
2695
+ signal: AbortSignal.timeout(3e3)
2696
+ });
2697
+ const data = await resp.json();
2698
+ const pins = data.discuss ?? [];
2699
+ const targets = pins.length ? pins : data.active ? [data.active] : [];
2700
+ return text(JSON.stringify({
2701
+ ...data,
2702
+ targets,
2703
+ targetSource: pins.length ? "pinned" : data.active ? "active-tab" : "none"
2704
+ }));
2705
+ } catch (err) {
2706
+ return text(JSON.stringify({ error: `Failed to read active session: ${err}` }));
2707
+ }
2708
+ }
2400
2709
  case "generate_contract": {
2401
2710
  const session = args.session;
2402
- const dir = (0, import_node_path5.join)(projectRoot, LAUNCHSECURE_DIR, "deck-files", session);
2403
- const manifestPath = (0, import_node_path5.join)(dir, "blast-radius-manifest.json");
2404
- if (!import_node_fs5.default.existsSync(manifestPath)) {
2711
+ const dir = (0, import_node_path6.join)(projectRoot, LAUNCHSECURE_DIR, "deck-files", session);
2712
+ const manifestPath = (0, import_node_path6.join)(dir, "blast-radius-manifest.json");
2713
+ if (!import_node_fs6.default.existsSync(manifestPath)) {
2405
2714
  return text(JSON.stringify({
2406
2715
  error: `No blast radius manifest found for session "${session}". Push a blast-radius block first.`,
2407
2716
  hint: "Use the deck tool with a blast-radius block type to create a session."
2408
2717
  }));
2409
2718
  }
2410
2719
  try {
2411
- const raw = import_node_fs5.default.readFileSync(manifestPath, "utf-8");
2720
+ const raw = import_node_fs6.default.readFileSync(manifestPath, "utf-8");
2412
2721
  const manifest = JSON.parse(raw);
2413
2722
  const contract = generateContract(manifest);
2414
2723
  const markdown = contractToMarkdown(contract);
2415
- import_node_fs5.default.writeFileSync((0, import_node_path5.join)(dir, "contract.json"), JSON.stringify(contract, null, 2));
2416
- import_node_fs5.default.writeFileSync((0, import_node_path5.join)(dir, "contract.md"), markdown);
2724
+ import_node_fs6.default.writeFileSync((0, import_node_path6.join)(dir, "contract.json"), JSON.stringify(contract, null, 2));
2725
+ import_node_fs6.default.writeFileSync((0, import_node_path6.join)(dir, "contract.md"), markdown);
2417
2726
  return text(JSON.stringify(contract));
2418
2727
  } catch (err) {
2419
2728
  return text(JSON.stringify({ error: `Failed to generate contract: ${err}` }));
@@ -2504,16 +2813,16 @@ function startDeckMcpServer() {
2504
2813
  process.exit(0);
2505
2814
  });
2506
2815
  }
2507
- var import_node_http2, import_node_fs5, import_node_path5, import_node_child_process2, import_node_fs6, import_node_os2, SERVER_INFO, TOOLS;
2816
+ var import_node_http2, import_node_fs6, import_node_path6, import_node_child_process2, import_node_fs7, import_node_os3, SERVER_INFO, TOOLS;
2508
2817
  var init_deck_mcp = __esm({
2509
2818
  "src/server/deck-mcp.ts"() {
2510
2819
  "use strict";
2511
2820
  import_node_http2 = __toESM(require("node:http"));
2512
- import_node_fs5 = __toESM(require("node:fs"));
2513
- import_node_path5 = require("node:path");
2821
+ import_node_fs6 = __toESM(require("node:fs"));
2822
+ import_node_path6 = require("node:path");
2514
2823
  import_node_child_process2 = require("node:child_process");
2515
- import_node_fs6 = require("node:fs");
2516
- import_node_os2 = require("node:os");
2824
+ import_node_fs7 = require("node:fs");
2825
+ import_node_os3 = require("node:os");
2517
2826
  init_launch_kit_paths();
2518
2827
  init_deck_lockfile();
2519
2828
  init_deck_config();
@@ -2637,6 +2946,11 @@ var init_deck_mcp = __esm({
2637
2946
  description: "Check whether the LaunchDeck UI server is running.",
2638
2947
  inputSchema: { type: "object", properties: {} }
2639
2948
  },
2949
+ {
2950
+ name: "active_session",
2951
+ 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".',
2952
+ inputSchema: { type: "object", properties: {} }
2953
+ },
2640
2954
  {
2641
2955
  name: "generate_contract",
2642
2956
  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.",
@@ -2657,8 +2971,92 @@ var init_deck_mcp = __esm({
2657
2971
 
2658
2972
  // src/server/deck-mcp-entry.ts
2659
2973
  init_deck_lockfile();
2974
+
2975
+ // src/server/prune-npx-cache.ts
2976
+ var import_node_fs2 = require("node:fs");
2977
+ var import_node_os2 = require("node:os");
2978
+ var import_node_path2 = require("node:path");
2979
+ var import_node_util = require("node:util");
2980
+ var PKG = "@launchsecure/launch-kit";
2981
+ var readFileAsync = (0, import_node_util.promisify)(import_node_fs2.readFile);
2982
+ var readdirAsync = (0, import_node_util.promisify)(import_node_fs2.readdir);
2983
+ var rmAsync = (0, import_node_util.promisify)(import_node_fs2.rm);
2984
+ var realpathAsync = (0, import_node_util.promisify)(import_node_fs2.realpath);
2985
+ function npxCacheRoot() {
2986
+ const cache = process.env.npm_config_cache;
2987
+ if (cache && cache.trim()) return (0, import_node_path2.join)(cache, "_npx");
2988
+ if (process.platform === "win32") {
2989
+ const localAppData = process.env.LOCALAPPDATA;
2990
+ if (localAppData) return (0, import_node_path2.join)(localAppData, "npm-cache", "_npx");
2991
+ }
2992
+ return (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".npm", "_npx");
2993
+ }
2994
+ function ownVersion() {
2995
+ let dir = __dirname;
2996
+ for (let i = 0; i < 8; i++) {
2997
+ try {
2998
+ const pkg = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path2.join)(dir, "package.json"), "utf8"));
2999
+ if (pkg && pkg.name === PKG) return typeof pkg.version === "string" ? pkg.version : null;
3000
+ } catch {
3001
+ }
3002
+ const parent = (0, import_node_path2.dirname)(dir);
3003
+ if (parent === dir) break;
3004
+ dir = parent;
3005
+ }
3006
+ return null;
3007
+ }
3008
+ async function pruneStaleNpxCache() {
3009
+ try {
3010
+ const version = ownVersion();
3011
+ if (!version) return;
3012
+ const root = npxCacheRoot();
3013
+ let hashes;
3014
+ try {
3015
+ hashes = await readdirAsync(root);
3016
+ } catch {
3017
+ return;
3018
+ }
3019
+ const selfPath = await realpathAsync(process.argv[1] || __dirname).catch(() => __dirname);
3020
+ let reaped = 0;
3021
+ for (const hash of hashes) {
3022
+ const dir = (0, import_node_path2.join)(root, hash);
3023
+ if (selfPath === dir || selfPath.startsWith(dir + import_node_path2.sep)) continue;
3024
+ let ver;
3025
+ try {
3026
+ const lkPkg = JSON.parse(
3027
+ await readFileAsync((0, import_node_path2.join)(dir, "node_modules", PKG, "package.json"), "utf8")
3028
+ );
3029
+ ver = lkPkg.version;
3030
+ } catch {
3031
+ continue;
3032
+ }
3033
+ if (ver === version) continue;
3034
+ try {
3035
+ const top = JSON.parse(await readFileAsync((0, import_node_path2.join)(dir, "package.json"), "utf8"));
3036
+ const spec = top?.dependencies?.[PKG];
3037
+ if (typeof spec === "string" && spec.startsWith("file:")) continue;
3038
+ } catch {
3039
+ }
3040
+ try {
3041
+ await rmAsync(dir, { recursive: true, force: true });
3042
+ reaped++;
3043
+ } catch {
3044
+ }
3045
+ }
3046
+ if (reaped > 0) {
3047
+ process.stderr.write(
3048
+ `[launch-kit] npx-cache: reaped ${reaped} stale copy(ies), kept v${version}
3049
+ `
3050
+ );
3051
+ }
3052
+ } catch {
3053
+ }
3054
+ }
3055
+
3056
+ // src/server/deck-mcp-entry.ts
2660
3057
  async function main() {
2661
3058
  setProjectRoot(process.cwd());
3059
+ void pruneStaleNpxCache();
2662
3060
  const argv = process.argv.slice(2);
2663
3061
  const subcommand = argv[0];
2664
3062
  if (subcommand === "serve") {