@launchsecure/launch-kit 0.0.15 → 0.0.16

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.
@@ -554,7 +554,7 @@ var require_claude_bridge = __commonJS({
554
554
  "use strict";
555
555
  var { spawn: spawn3 } = require("node-pty");
556
556
  var path9 = require("path");
557
- var fs9 = require("fs");
557
+ var fs10 = require("fs");
558
558
  var ClaudeBridge = class {
559
559
  constructor() {
560
560
  this.sessions = /* @__PURE__ */ new Map();
@@ -572,7 +572,7 @@ var require_claude_bridge = __commonJS({
572
572
  ];
573
573
  for (const cmd of possibleCommands) {
574
574
  try {
575
- if (fs9.existsSync(cmd) || this.commandExists(cmd)) {
575
+ if (fs10.existsSync(cmd) || this.commandExists(cmd)) {
576
576
  console.log(`Found Claude command at: ${cmd}`);
577
577
  return cmd;
578
578
  }
@@ -773,7 +773,7 @@ var require_codex_bridge = __commonJS({
773
773
  "use strict";
774
774
  var { spawn: spawn3 } = require("node-pty");
775
775
  var path9 = require("path");
776
- var fs9 = require("fs");
776
+ var fs10 = require("fs");
777
777
  var CodexBridge = class {
778
778
  constructor() {
779
779
  this.sessions = /* @__PURE__ */ new Map();
@@ -790,7 +790,7 @@ var require_codex_bridge = __commonJS({
790
790
  ];
791
791
  for (const cmd of possibleCommands) {
792
792
  try {
793
- if (fs9.existsSync(cmd) || this.commandExists(cmd)) {
793
+ if (fs10.existsSync(cmd) || this.commandExists(cmd)) {
794
794
  console.log(`Found Codex command at: ${cmd}`);
795
795
  return cmd;
796
796
  }
@@ -966,7 +966,7 @@ var require_agent_bridge = __commonJS({
966
966
  "use strict";
967
967
  var { spawn: spawn3 } = require("node-pty");
968
968
  var path9 = require("path");
969
- var fs9 = require("fs");
969
+ var fs10 = require("fs");
970
970
  var AgentBridge = class {
971
971
  constructor() {
972
972
  this.sessions = /* @__PURE__ */ new Map();
@@ -982,7 +982,7 @@ var require_agent_bridge = __commonJS({
982
982
  ];
983
983
  for (const cmd of possibleCommands) {
984
984
  try {
985
- if (fs9.existsSync(cmd) || this.commandExists(cmd)) {
985
+ if (fs10.existsSync(cmd) || this.commandExists(cmd)) {
986
986
  console.log(`Found Agent command at: ${cmd}`);
987
987
  return cmd;
988
988
  }
@@ -1306,7 +1306,7 @@ var require_script_bridge = __commonJS({
1306
1306
  var require_session_store = __commonJS({
1307
1307
  "../claude-code-web/src/utils/session-store.js"(exports2, module2) {
1308
1308
  "use strict";
1309
- var fs9 = require("fs").promises;
1309
+ var fs10 = require("fs").promises;
1310
1310
  var path9 = require("path");
1311
1311
  var os2 = require("os");
1312
1312
  var SessionStore = class {
@@ -1317,14 +1317,14 @@ var require_session_store = __commonJS({
1317
1317
  }
1318
1318
  async initializeStorage() {
1319
1319
  try {
1320
- await fs9.mkdir(this.storageDir, { recursive: true });
1320
+ await fs10.mkdir(this.storageDir, { recursive: true });
1321
1321
  } catch (error) {
1322
1322
  console.error("Failed to create storage directory:", error);
1323
1323
  }
1324
1324
  }
1325
1325
  async saveSessions(sessions) {
1326
1326
  try {
1327
- await fs9.mkdir(this.storageDir, { recursive: true });
1327
+ await fs10.mkdir(this.storageDir, { recursive: true });
1328
1328
  const sessionsArray = Array.from(sessions.entries()).map(([id, session]) => ({
1329
1329
  id,
1330
1330
  name: session.name || "Unnamed Session",
@@ -1355,9 +1355,9 @@ var require_session_store = __commonJS({
1355
1355
  sessions: sessionsArray
1356
1356
  };
1357
1357
  const tempFile = `${this.sessionsFile}.tmp`;
1358
- await fs9.writeFile(tempFile, JSON.stringify(data, null, 2));
1359
- await fs9.mkdir(this.storageDir, { recursive: true });
1360
- await fs9.rename(tempFile, this.sessionsFile);
1358
+ await fs10.writeFile(tempFile, JSON.stringify(data, null, 2));
1359
+ await fs10.mkdir(this.storageDir, { recursive: true });
1360
+ await fs10.rename(tempFile, this.sessionsFile);
1361
1361
  return true;
1362
1362
  } catch (error) {
1363
1363
  console.error("Failed to save sessions:", error.message);
@@ -1366,8 +1366,8 @@ var require_session_store = __commonJS({
1366
1366
  }
1367
1367
  async loadSessions() {
1368
1368
  try {
1369
- await fs9.access(this.sessionsFile);
1370
- const data = await fs9.readFile(this.sessionsFile, "utf8");
1369
+ await fs10.access(this.sessionsFile);
1370
+ const data = await fs10.readFile(this.sessionsFile, "utf8");
1371
1371
  if (!data || !data.trim()) {
1372
1372
  console.log("Sessions file is empty, starting fresh");
1373
1373
  return /* @__PURE__ */ new Map();
@@ -1378,7 +1378,7 @@ var require_session_store = __commonJS({
1378
1378
  } catch (parseError) {
1379
1379
  console.error("Sessions file is corrupted, starting fresh:", parseError.message);
1380
1380
  try {
1381
- await fs9.rename(this.sessionsFile, `${this.sessionsFile}.corrupted.${Date.now()}`);
1381
+ await fs10.rename(this.sessionsFile, `${this.sessionsFile}.corrupted.${Date.now()}`);
1382
1382
  } catch (renameError) {
1383
1383
  }
1384
1384
  return /* @__PURE__ */ new Map();
@@ -1422,7 +1422,7 @@ var require_session_store = __commonJS({
1422
1422
  }
1423
1423
  async clearOldSessions() {
1424
1424
  try {
1425
- await fs9.unlink(this.sessionsFile);
1425
+ await fs10.unlink(this.sessionsFile);
1426
1426
  console.log("Cleared old sessions");
1427
1427
  return true;
1428
1428
  } catch (error) {
@@ -1434,9 +1434,9 @@ var require_session_store = __commonJS({
1434
1434
  }
1435
1435
  async getSessionMetadata() {
1436
1436
  try {
1437
- await fs9.access(this.sessionsFile);
1438
- const stats = await fs9.stat(this.sessionsFile);
1439
- const data = await fs9.readFile(this.sessionsFile, "utf8");
1437
+ await fs10.access(this.sessionsFile);
1438
+ const stats = await fs10.stat(this.sessionsFile);
1439
+ const data = await fs10.readFile(this.sessionsFile, "utf8");
1440
1440
  const parsed = JSON.parse(data);
1441
1441
  return {
1442
1442
  exists: true,
@@ -1461,7 +1461,7 @@ var require_session_store = __commonJS({
1461
1461
  var require_usage_reader = __commonJS({
1462
1462
  "../claude-code-web/src/usage-reader.js"(exports2, module2) {
1463
1463
  "use strict";
1464
- var fs9 = require("fs").promises;
1464
+ var fs10 = require("fs").promises;
1465
1465
  var path9 = require("path");
1466
1466
  var readline = require("readline");
1467
1467
  var { createReadStream } = require("fs");
@@ -1669,12 +1669,12 @@ var require_usage_reader = __commonJS({
1669
1669
  const projectDirName = cwd.replace(/\//g, "-");
1670
1670
  let projectPath = path9.join(this.claudeProjectsPath, projectDirName);
1671
1671
  try {
1672
- await fs9.access(projectPath);
1672
+ await fs10.access(projectPath);
1673
1673
  } catch (err2) {
1674
1674
  console.log(`Project directory not found: ${projectPath}`);
1675
1675
  return null;
1676
1676
  }
1677
- const files = await fs9.readdir(projectPath);
1677
+ const files = await fs10.readdir(projectPath);
1678
1678
  const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
1679
1679
  if (jsonlFiles.length === 0) {
1680
1680
  return null;
@@ -1683,7 +1683,7 @@ var require_usage_reader = __commonJS({
1683
1683
  let mostRecentTime = 0;
1684
1684
  for (const file of jsonlFiles) {
1685
1685
  const filePath = path9.join(projectPath, file);
1686
- const stat = await fs9.stat(filePath);
1686
+ const stat = await fs10.stat(filePath);
1687
1687
  if (stat.mtime.getTime() > mostRecentTime) {
1688
1688
  mostRecentTime = stat.mtime.getTime();
1689
1689
  mostRecentFile = filePath;
@@ -1698,17 +1698,17 @@ var require_usage_reader = __commonJS({
1698
1698
  async findJsonlFiles(onlyRecent = false) {
1699
1699
  const files = [];
1700
1700
  try {
1701
- const projectDirs = await fs9.readdir(this.claudeProjectsPath);
1701
+ const projectDirs = await fs10.readdir(this.claudeProjectsPath);
1702
1702
  for (const projectDir of projectDirs) {
1703
1703
  const projectPath = path9.join(this.claudeProjectsPath, projectDir);
1704
- const stat = await fs9.stat(projectPath);
1704
+ const stat = await fs10.stat(projectPath);
1705
1705
  if (stat.isDirectory()) {
1706
- const projectFiles = await fs9.readdir(projectPath);
1706
+ const projectFiles = await fs10.readdir(projectPath);
1707
1707
  const jsonlFiles = projectFiles.filter((f) => f.endsWith(".jsonl"));
1708
1708
  for (const jsonlFile of jsonlFiles) {
1709
1709
  const filePath = path9.join(projectPath, jsonlFile);
1710
1710
  if (onlyRecent) {
1711
- const fileStat = await fs9.stat(filePath);
1711
+ const fileStat = await fs10.stat(filePath);
1712
1712
  const hoursSinceModified = (Date.now() - fileStat.mtime.getTime()) / (1e3 * 60 * 60);
1713
1713
  if (hoursSinceModified <= 24) {
1714
1714
  files.push(filePath);
@@ -1871,7 +1871,7 @@ var require_usage_reader = __commonJS({
1871
1871
  }
1872
1872
  const sessionFile = path9.join(this.claudeProjectsPath, path9.basename(process.cwd()).replace(/[^a-zA-Z0-9-]/g, "-"), `${sessionId}.jsonl`);
1873
1873
  try {
1874
- await fs9.access(sessionFile);
1874
+ await fs10.access(sessionFile);
1875
1875
  } catch (err2) {
1876
1876
  return null;
1877
1877
  }
@@ -2537,7 +2537,7 @@ var require_src = __commonJS({
2537
2537
  var SessionStore = require_session_store();
2538
2538
  var UsageReader = require_usage_reader();
2539
2539
  var UsageAnalytics = require_usage_analytics();
2540
- var fs9 = require("fs");
2540
+ var fs10 = require("fs");
2541
2541
  function stripAnsi(str) {
2542
2542
  return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "").replace(/\x1b[()][AB012]/g, "").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "").replace(/\r\n?/g, "\n").trim();
2543
2543
  }
@@ -2795,7 +2795,7 @@ var require_src = __commonJS({
2795
2795
  return true;
2796
2796
  }
2797
2797
  try {
2798
- const items = fs9.readdirSync(validation.path, { withFileTypes: true });
2798
+ const items = fs10.readdirSync(validation.path, { withFileTypes: true });
2799
2799
  const showHidden = url.searchParams.get("showHidden") === "true";
2800
2800
  const folders = items.filter((item) => item.isDirectory()).filter((item) => !item.name.startsWith(".") || showHidden).map((item) => ({
2801
2801
  name: item.name,
@@ -2830,7 +2830,7 @@ var require_src = __commonJS({
2830
2830
  res.end(JSON.stringify({ error: validation.error }));
2831
2831
  return;
2832
2832
  }
2833
- if (!fs9.existsSync(validation.path) || !fs9.statSync(validation.path).isDirectory()) {
2833
+ if (!fs10.existsSync(validation.path) || !fs10.statSync(validation.path).isDirectory()) {
2834
2834
  res.writeHead(400, { "Content-Type": "application/json" });
2835
2835
  res.end(JSON.stringify({ error: "Not a valid directory" }));
2836
2836
  return;
@@ -2857,7 +2857,7 @@ var require_src = __commonJS({
2857
2857
  res.end(JSON.stringify({ error: validation.error }));
2858
2858
  return;
2859
2859
  }
2860
- if (!fs9.existsSync(validation.path) || !fs9.statSync(validation.path).isDirectory()) {
2860
+ if (!fs10.existsSync(validation.path) || !fs10.statSync(validation.path).isDirectory()) {
2861
2861
  res.writeHead(400, { "Content-Type": "application/json" });
2862
2862
  res.end(JSON.stringify({ error: "Invalid directory path" }));
2863
2863
  return;
@@ -2892,12 +2892,12 @@ var require_src = __commonJS({
2892
2892
  res.end(JSON.stringify({ message: "Cannot create folder outside the allowed area" }));
2893
2893
  return;
2894
2894
  }
2895
- if (fs9.existsSync(fullValidation.path)) {
2895
+ if (fs10.existsSync(fullValidation.path)) {
2896
2896
  res.writeHead(409, { "Content-Type": "application/json" });
2897
2897
  res.end(JSON.stringify({ message: "Folder already exists" }));
2898
2898
  return;
2899
2899
  }
2900
- fs9.mkdirSync(fullValidation.path, { recursive: true });
2900
+ fs10.mkdirSync(fullValidation.path, { recursive: true });
2901
2901
  res.writeHead(200, { "Content-Type": "application/json" });
2902
2902
  res.end(JSON.stringify({ success: true, path: fullValidation.path }));
2903
2903
  } catch (e) {
@@ -2923,7 +2923,7 @@ var require_src = __commonJS({
2923
2923
  if (!filePath || filePath === "") filePath = "index.html";
2924
2924
  const fullPath = path9.join(__dirname, "public", filePath);
2925
2925
  try {
2926
- if (!fs9.existsSync(fullPath)) return false;
2926
+ if (!fs10.existsSync(fullPath)) return false;
2927
2927
  const ext = path9.extname(fullPath);
2928
2928
  const mimeTypes = {
2929
2929
  ".html": "text/html",
@@ -2935,7 +2935,7 @@ var require_src = __commonJS({
2935
2935
  ".ico": "image/x-icon"
2936
2936
  };
2937
2937
  const mime = mimeTypes[ext] || "application/octet-stream";
2938
- const content = fs9.readFileSync(fullPath);
2938
+ const content = fs10.readFileSync(fullPath);
2939
2939
  res.writeHead(200, { "Content-Type": mime });
2940
2940
  res.end(content);
2941
2941
  return true;
@@ -4363,6 +4363,7 @@ function shutdownTerminalBridge() {
4363
4363
  // src/server/mcp-config-writer.ts
4364
4364
  var import_fs = __toESM(require("fs"));
4365
4365
  var import_path = __toESM(require("path"));
4366
+ var MANAGED_SERVERS = ["launch-pod", "launch-chart"];
4366
4367
  var writtenPaths = [];
4367
4368
  function writeMcpConfigs(opts) {
4368
4369
  const { projectDir, token, serverUrl } = opts;
@@ -4373,7 +4374,10 @@ function writeMcpConfigs(opts) {
4373
4374
  function removeMcpConfigs() {
4374
4375
  for (const filePath of writtenPaths) {
4375
4376
  try {
4376
- if (import_fs.default.existsSync(filePath)) {
4377
+ if (!import_fs.default.existsSync(filePath)) continue;
4378
+ if (filePath.endsWith(".mcp.json")) {
4379
+ removeClaudeEntries(filePath);
4380
+ } else {
4377
4381
  import_fs.default.unlinkSync(filePath);
4378
4382
  console.log(`MCP config removed: ${filePath}`);
4379
4383
  }
@@ -4382,46 +4386,88 @@ function removeMcpConfigs() {
4382
4386
  }
4383
4387
  writtenPaths.length = 0;
4384
4388
  }
4389
+ function removeClaudeEntries(filePath) {
4390
+ try {
4391
+ const raw = import_fs.default.readFileSync(filePath, "utf-8");
4392
+ const existing = JSON.parse(raw);
4393
+ const servers = existing.mcpServers ?? {};
4394
+ for (const key of MANAGED_SERVERS) {
4395
+ delete servers[key];
4396
+ }
4397
+ if (Object.keys(servers).length === 0 && Object.keys(existing).length <= 1) {
4398
+ import_fs.default.unlinkSync(filePath);
4399
+ console.log(`MCP config removed (empty): ${filePath}`);
4400
+ } else {
4401
+ existing.mcpServers = servers;
4402
+ import_fs.default.writeFileSync(filePath, JSON.stringify(existing, null, 2) + "\n", { mode: 384 });
4403
+ console.log(`MCP config cleaned (LaunchPod entries removed): ${filePath}`);
4404
+ }
4405
+ } catch {
4406
+ try {
4407
+ import_fs.default.unlinkSync(filePath);
4408
+ } catch {
4409
+ }
4410
+ console.log(`MCP config removed: ${filePath}`);
4411
+ }
4412
+ }
4385
4413
  function writeClaudeConfig(projectDir, mcpUrl, token) {
4386
- const config = {
4387
- mcpServers: {
4388
- "launch-pod": {
4389
- type: "http",
4390
- url: mcpUrl,
4391
- headers: {
4392
- Authorization: `Bearer ${token}`
4393
- }
4394
- },
4395
- "launch-chart": {
4396
- command: "launch-chart",
4397
- args: []
4398
- }
4414
+ const filePath = import_path.default.join(projectDir, ".mcp.json");
4415
+ let existing = {};
4416
+ try {
4417
+ if (import_fs.default.existsSync(filePath)) {
4418
+ existing = JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
4419
+ }
4420
+ } catch {
4421
+ console.warn(`Warning: existing .mcp.json was malformed, preserving as .mcp.json.bak`);
4422
+ try {
4423
+ import_fs.default.copyFileSync(filePath, filePath + ".bak");
4424
+ } catch {
4425
+ }
4426
+ }
4427
+ const servers = existing.mcpServers ?? {};
4428
+ servers["launch-pod"] = {
4429
+ type: "http",
4430
+ url: mcpUrl,
4431
+ headers: {
4432
+ Authorization: `Bearer ${token}`
4399
4433
  }
4400
4434
  };
4401
- const filePath = import_path.default.join(projectDir, ".mcp.json");
4402
- import_fs.default.writeFileSync(filePath, JSON.stringify(config, null, 2), { mode: 384 });
4435
+ servers["launch-chart"] = {
4436
+ command: "launch-chart",
4437
+ args: []
4438
+ };
4439
+ existing.mcpServers = servers;
4440
+ import_fs.default.writeFileSync(filePath, JSON.stringify(existing, null, 2) + "\n", { mode: 384 });
4403
4441
  writtenPaths.push(filePath);
4404
- console.log(`Claude Code MCP config written to ${filePath}`);
4442
+ console.log(`Claude Code MCP config merged into ${filePath}`);
4405
4443
  }
4406
4444
  function writeCodexConfig(projectDir, mcpUrl, token) {
4407
4445
  const codexDir = import_path.default.join(projectDir, ".codex");
4408
4446
  if (!import_fs.default.existsSync(codexDir)) {
4409
4447
  import_fs.default.mkdirSync(codexDir, { recursive: true, mode: 448 });
4410
4448
  }
4411
- const toml = [
4449
+ const filePath = import_path.default.join(codexDir, "config.toml");
4450
+ let existingContent = "";
4451
+ try {
4452
+ if (import_fs.default.existsSync(filePath)) {
4453
+ existingContent = import_fs.default.readFileSync(filePath, "utf-8");
4454
+ }
4455
+ } catch {
4456
+ }
4457
+ const cleaned = existingContent.replace(/\[mcp_servers\.launch-pod\][^\[]*/, "").replace(/\[mcp_servers\.launch-chart\][^\[]*/, "").trim();
4458
+ const launchPodBlock = [
4412
4459
  `[mcp_servers.launch-pod]`,
4413
4460
  `url = "${mcpUrl}"`,
4414
4461
  `http_headers = { "Authorization" = "Bearer ${token}" }`,
4415
4462
  ``,
4416
4463
  `[mcp_servers.launch-chart]`,
4417
4464
  `command = "launch-chart"`,
4418
- `args = []`,
4419
- ``
4465
+ `args = []`
4420
4466
  ].join("\n");
4421
- const filePath = import_path.default.join(codexDir, "config.toml");
4422
- import_fs.default.writeFileSync(filePath, toml, { mode: 384 });
4467
+ const merged = cleaned ? cleaned + "\n\n" + launchPodBlock + "\n" : launchPodBlock + "\n";
4468
+ import_fs.default.writeFileSync(filePath, merged, { mode: 384 });
4423
4469
  writtenPaths.push(filePath);
4424
- console.log(`Codex MCP config written to ${filePath}`);
4470
+ console.log(`Codex MCP config merged into ${filePath}`);
4425
4471
  }
4426
4472
 
4427
4473
  // src/server/tracker-poller.ts
@@ -4528,13 +4574,18 @@ All pipeline data lives in the **database**, not on the local filesystem.
4528
4574
  ---
4529
4575
 
4530
4576
  `;
4531
- function resolvePrompt(agentId, unitId, inputs) {
4532
- const hyphenated = agentId.replace(/_/g, "-");
4533
- const filePath = import_path2.default.join(PROMPTS_DIR, `${hyphenated}.md`);
4534
- if (!import_fs2.default.existsSync(filePath)) {
4535
- throw new Error(`Prompt file not found for agent "${agentId}": ${filePath}`);
4577
+ function resolvePrompt(agentId, unitId, inputs, promptOverride) {
4578
+ let raw;
4579
+ if (promptOverride) {
4580
+ raw = promptOverride;
4581
+ } else {
4582
+ const hyphenated = agentId.replace(/_/g, "-");
4583
+ const filePath = import_path2.default.join(PROMPTS_DIR, `${hyphenated}.md`);
4584
+ if (!import_fs2.default.existsSync(filePath)) {
4585
+ throw new Error(`Prompt file not found for agent "${agentId}": ${filePath}`);
4586
+ }
4587
+ raw = import_fs2.default.readFileSync(filePath, "utf-8");
4536
4588
  }
4537
- const raw = import_fs2.default.readFileSync(filePath, "utf-8");
4538
4589
  const unitPreamble = unitId ? `
4539
4590
  ---
4540
4591
 
@@ -4653,7 +4704,7 @@ var NodeTriggerController = class {
4653
4704
  async startSession(agentId, agent, unitId) {
4654
4705
  const key = this.sessionKey(agentId, unitId);
4655
4706
  try {
4656
- const prompt = resolvePrompt(agentId, unitId, agent.in);
4707
+ const prompt = resolvePrompt(agentId, unitId, agent.in, agent.po);
4657
4708
  const sessionId = await this.adapter.start({
4658
4709
  agentId,
4659
4710
  prompt,
@@ -10099,35 +10150,237 @@ async function handleGraphCommand(subcommand, args) {
10099
10150
  }
10100
10151
 
10101
10152
  // src/server/graph-mcp.ts
10102
- var import_node_fs18 = require("node:fs");
10103
- var import_node_path20 = require("node:path");
10153
+ var import_node_fs19 = require("node:fs");
10154
+ var import_node_path21 = require("node:path");
10104
10155
  var import_node_child_process2 = require("node:child_process");
10105
10156
  var import_node_os2 = require("node:os");
10106
10157
 
10158
+ // src/server/blast-radius-builder.ts
10159
+ var import_node_fs15 = __toESM(require("node:fs"));
10160
+ var import_node_path17 = require("node:path");
10161
+ var FALLBACK_DEFAULTS = {
10162
+ rings: [
10163
+ { id: "modify", name: "Modify", color: "#ff6b00" },
10164
+ { id: "ripple", name: "Ripple (verify)", color: "#ffff00" },
10165
+ { id: "create", name: "Create", color: "#00ff00" }
10166
+ ],
10167
+ layers: {
10168
+ db: { name: "Database", icon: "database", color: "#cbd5e1" },
10169
+ api: { name: "API", icon: "server", color: "#cbd5e1" },
10170
+ middleware: { name: "Middleware", icon: "shield", color: "#cbd5e1" },
10171
+ ui: { name: "UI", icon: "layout-dashboard", color: "#cbd5e1" },
10172
+ config: { name: "Config / Seed", icon: "settings", color: "#cbd5e1" },
10173
+ shared: { name: "Shared Types", icon: "box", color: "#cbd5e1" }
10174
+ },
10175
+ center: { color: "#ff0000" }
10176
+ };
10177
+ function loadDefaults(rootDir) {
10178
+ const filePath = (0, import_node_path17.join)(rootDir, ".launchsecure", "blast-radius-defaults.json");
10179
+ try {
10180
+ if (import_node_fs15.default.existsSync(filePath)) {
10181
+ const raw = import_node_fs15.default.readFileSync(filePath, "utf-8");
10182
+ return JSON.parse(raw);
10183
+ }
10184
+ } catch {
10185
+ }
10186
+ return FALLBACK_DEFAULTS;
10187
+ }
10188
+ function generateAcceptance(node, inspect) {
10189
+ const criteria = [];
10190
+ const t = node.type?.toLowerCase() ?? "";
10191
+ if (t === "endpoint" || t === "mcp-tool") {
10192
+ const methods = inspect?.methods ?? [];
10193
+ const path9 = inspect?.path ?? node.id;
10194
+ if (methods.length > 0) {
10195
+ criteria.push(`${methods.join("/")} ${path9} still returns correct responses for authorized users`);
10196
+ } else {
10197
+ criteria.push(`${path9} still responds correctly`);
10198
+ }
10199
+ if (inspect?.auth && inspect.auth.includes("withAuth")) {
10200
+ criteria.push("Authentication and authorization still enforced");
10201
+ }
10202
+ if (inspect?.db_models && inspect.db_models.length > 0) {
10203
+ criteria.push(`DB operations on ${inspect.db_models.join(", ")} still work correctly`);
10204
+ }
10205
+ } else if (t === "page" || t === "component" || t === "layout") {
10206
+ criteria.push(`${node.name} renders without errors`);
10207
+ if (inspect?.stateVars && inspect.stateVars.length > 0) {
10208
+ criteria.push("State management still works correctly");
10209
+ }
10210
+ if (inspect?.elements && inspect.elements.length > 5) {
10211
+ criteria.push("All child components render correctly");
10212
+ }
10213
+ } else if (t === "table" || t === "enum") {
10214
+ criteria.push(`${node.name} schema unchanged or migration applies cleanly`);
10215
+ criteria.push("Existing queries against this table still work");
10216
+ } else if (t === "hook") {
10217
+ criteria.push(`${node.name} returns expected shape`);
10218
+ if (inspect?.stateVars && inspect.stateVars.length > 0) {
10219
+ criteria.push(`State variables [${inspect.stateVars.map((s) => s.name).join(", ")}] still returned`);
10220
+ }
10221
+ } else if (t === "context") {
10222
+ criteria.push(`${node.name} provides correct context to consumers`);
10223
+ } else if (t === "lib" || t === "config" || t === "types") {
10224
+ criteria.push(`${node.name} exports still conform to expected interface`);
10225
+ } else if (t === "seed" || t === "seed_role" || t === "seed_permission") {
10226
+ criteria.push("Seed runs without errors");
10227
+ criteria.push("Expected rows created in database");
10228
+ } else {
10229
+ criteria.push("Verify no regression");
10230
+ }
10231
+ return criteria;
10232
+ }
10233
+ function buildManifest(input) {
10234
+ const { mode, title, description, subtitle, blastResults, createNodes, inspectData, defaults } = input;
10235
+ const nodeMap = /* @__PURE__ */ new Map();
10236
+ const centerNodeIds = /* @__PURE__ */ new Set();
10237
+ for (const result of blastResults) {
10238
+ centerNodeIds.add(result.center.id);
10239
+ for (const node of result.affected) {
10240
+ const existing = nodeMap.get(node.id);
10241
+ if (!existing || node.hop < existing.hop) {
10242
+ nodeMap.set(node.id, node);
10243
+ }
10244
+ }
10245
+ }
10246
+ for (const id of centerNodeIds) {
10247
+ nodeMap.delete(id);
10248
+ }
10249
+ const manifestNodes = [];
10250
+ for (const result of blastResults) {
10251
+ const c = result.center;
10252
+ if (manifestNodes.some((n) => n.id === c.id)) continue;
10253
+ const inspect = inspectData[c.id];
10254
+ manifestNodes.push({
10255
+ id: c.id,
10256
+ name: c.name,
10257
+ layer: c.layer,
10258
+ ring: "modify",
10259
+ type: c.type,
10260
+ reason: `Direct change target`,
10261
+ acceptance: generateAcceptance(
10262
+ { id: c.id, name: c.name, type: c.type, layer: c.layer, hop: 0 },
10263
+ inspect
10264
+ )
10265
+ });
10266
+ }
10267
+ for (const [, node] of nodeMap) {
10268
+ const ring = node.hop <= 1 ? "modify" : "ripple";
10269
+ const inspect = inspectData[node.id];
10270
+ const reason = node.hop <= 1 ? `Directly depends on changed node` : `Indirect dependency (${node.hop} hops away)`;
10271
+ manifestNodes.push({
10272
+ id: node.id,
10273
+ name: node.name,
10274
+ layer: node.layer,
10275
+ ring,
10276
+ type: node.type,
10277
+ reason,
10278
+ acceptance: generateAcceptance(node, inspect)
10279
+ });
10280
+ }
10281
+ for (const cn of createNodes) {
10282
+ manifestNodes.push({
10283
+ id: cn.id,
10284
+ name: cn.name,
10285
+ layer: cn.layer,
10286
+ ring: "create",
10287
+ type: cn.type ?? "unknown",
10288
+ reason: cn.reason,
10289
+ acceptance: cn.acceptance ?? ["Verify implementation matches spec"]
10290
+ });
10291
+ }
10292
+ const layerIds = /* @__PURE__ */ new Set();
10293
+ for (const n of manifestNodes) {
10294
+ layerIds.add(n.layer);
10295
+ }
10296
+ const layers = [];
10297
+ for (const id of layerIds) {
10298
+ const def = defaults.layers[id];
10299
+ if (def) {
10300
+ layers.push({ id, name: def.name, icon: def.icon, color: def.color });
10301
+ } else {
10302
+ layers.push({ id, name: id, icon: "box", color: "#cbd5e1" });
10303
+ }
10304
+ }
10305
+ const edgeSet = /* @__PURE__ */ new Set();
10306
+ const edges = [];
10307
+ const allNodeIds = new Set(manifestNodes.map((n) => n.id));
10308
+ for (const cId of centerNodeIds) {
10309
+ for (const result of blastResults) {
10310
+ for (const affected of result.affected) {
10311
+ if (affected.hop === 1 && result.center.id === cId && allNodeIds.has(affected.id)) {
10312
+ const key = `${cId}->${affected.id}`;
10313
+ if (!edgeSet.has(key)) {
10314
+ edgeSet.add(key);
10315
+ edges.push({ source: cId, target: affected.id });
10316
+ }
10317
+ }
10318
+ }
10319
+ }
10320
+ }
10321
+ for (const result of blastResults) {
10322
+ if (result.edges) {
10323
+ for (const edge of result.edges) {
10324
+ if (allNodeIds.has(edge.source) && allNodeIds.has(edge.target)) {
10325
+ const key = `${edge.source}->${edge.target}`;
10326
+ if (!edgeSet.has(key)) {
10327
+ edgeSet.add(key);
10328
+ edges.push({ source: edge.source, target: edge.target });
10329
+ }
10330
+ }
10331
+ }
10332
+ }
10333
+ }
10334
+ for (const cn of createNodes) {
10335
+ edges.push({ source: "center", target: cn.id });
10336
+ if (cn.connects_to) {
10337
+ for (const targetId of cn.connects_to) {
10338
+ if (allNodeIds.has(targetId) || createNodes.some((c) => c.id === targetId)) {
10339
+ const key = `${cn.id}->${targetId}`;
10340
+ if (!edgeSet.has(key)) {
10341
+ edgeSet.add(key);
10342
+ edges.push({ source: cn.id, target: targetId });
10343
+ }
10344
+ }
10345
+ }
10346
+ }
10347
+ }
10348
+ return {
10349
+ mode,
10350
+ title,
10351
+ subtitle,
10352
+ layers,
10353
+ rings: defaults.rings,
10354
+ center: { name: title, description },
10355
+ nodes: manifestNodes,
10356
+ edges
10357
+ };
10358
+ }
10359
+
10107
10360
  // src/server/lockfile.ts
10108
10361
  var import_node_child_process = require("node:child_process");
10109
- var import_node_fs15 = require("node:fs");
10362
+ var import_node_fs16 = require("node:fs");
10110
10363
  var import_node_os = require("node:os");
10111
- var import_node_path17 = require("node:path");
10364
+ var import_node_path18 = require("node:path");
10112
10365
  function lockDir(projectRoot) {
10113
10366
  if (projectRoot) {
10114
- return (0, import_node_path17.join)(projectRoot, ".launchsecure");
10367
+ return (0, import_node_path18.join)(projectRoot, ".launchsecure");
10115
10368
  }
10116
- return (0, import_node_path17.join)((0, import_node_os.homedir)(), ".launchsecure");
10369
+ return (0, import_node_path18.join)((0, import_node_os.homedir)(), ".launchsecure");
10117
10370
  }
10118
10371
  function lockPath(projectRoot) {
10119
- return (0, import_node_path17.join)(lockDir(projectRoot), "launch-chart.lock");
10372
+ return (0, import_node_path18.join)(lockDir(projectRoot), "launch-chart.lock");
10120
10373
  }
10121
10374
  var _activeProjectRoot;
10122
10375
  function readLock(projectRoot) {
10123
10376
  const root = projectRoot ?? _activeProjectRoot;
10124
10377
  const p = lockPath(root);
10125
- if (!(0, import_node_fs15.existsSync)(p)) {
10378
+ if (!(0, import_node_fs16.existsSync)(p)) {
10126
10379
  if (root) {
10127
10380
  const globalP = lockPath();
10128
- if ((0, import_node_fs15.existsSync)(globalP)) {
10381
+ if ((0, import_node_fs16.existsSync)(globalP)) {
10129
10382
  try {
10130
- const data = JSON.parse((0, import_node_fs15.readFileSync)(globalP, "utf-8"));
10383
+ const data = JSON.parse((0, import_node_fs16.readFileSync)(globalP, "utf-8"));
10131
10384
  if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
10132
10385
  return data;
10133
10386
  }
@@ -10138,7 +10391,7 @@ function readLock(projectRoot) {
10138
10391
  return null;
10139
10392
  }
10140
10393
  try {
10141
- const data = JSON.parse((0, import_node_fs15.readFileSync)(p, "utf-8"));
10394
+ const data = JSON.parse((0, import_node_fs16.readFileSync)(p, "utf-8"));
10142
10395
  if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
10143
10396
  return data;
10144
10397
  } catch {
@@ -10175,7 +10428,7 @@ function getLiveLock(projectRoot) {
10175
10428
  const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
10176
10429
  if (!live) {
10177
10430
  try {
10178
- (0, import_node_fs15.unlinkSync)(lockPath(root));
10431
+ (0, import_node_fs16.unlinkSync)(lockPath(root));
10179
10432
  } catch {
10180
10433
  }
10181
10434
  return null;
@@ -10185,7 +10438,7 @@ function getLiveLock(projectRoot) {
10185
10438
  function clearLock(projectRoot) {
10186
10439
  const root = projectRoot ?? _activeProjectRoot;
10187
10440
  try {
10188
- (0, import_node_fs15.unlinkSync)(lockPath(root));
10441
+ (0, import_node_fs16.unlinkSync)(lockPath(root));
10189
10442
  } catch {
10190
10443
  }
10191
10444
  }
@@ -10194,8 +10447,8 @@ function clearLock(projectRoot) {
10194
10447
  init_config();
10195
10448
 
10196
10449
  // src/server/graph/core/language-detection.ts
10197
- var import_node_fs16 = require("node:fs");
10198
- var import_node_path18 = require("node:path");
10450
+ var import_node_fs17 = require("node:fs");
10451
+ var import_node_path19 = require("node:path");
10199
10452
  var EXTENSION_TO_LANGUAGE = {
10200
10453
  // Web / Frontend
10201
10454
  ".ts": "typescript",
@@ -10307,10 +10560,10 @@ var AUXILIARY_LANGUAGES = /* @__PURE__ */ new Set([
10307
10560
  ]);
10308
10561
  function walkForExtensions(dir, extCounts, depth = 0) {
10309
10562
  if (depth > 10) return;
10310
- if (!(0, import_node_fs16.existsSync)(dir)) return;
10563
+ if (!(0, import_node_fs17.existsSync)(dir)) return;
10311
10564
  let entries;
10312
10565
  try {
10313
- entries = (0, import_node_fs16.readdirSync)(dir, { withFileTypes: true });
10566
+ entries = (0, import_node_fs17.readdirSync)(dir, { withFileTypes: true });
10314
10567
  } catch {
10315
10568
  return;
10316
10569
  }
@@ -10318,9 +10571,9 @@ function walkForExtensions(dir, extCounts, depth = 0) {
10318
10571
  if (entry.name.startsWith(".") && entry.isDirectory()) continue;
10319
10572
  if (entry.isDirectory()) {
10320
10573
  if (IGNORE_DIRS.has(entry.name)) continue;
10321
- walkForExtensions((0, import_node_path18.join)(dir, entry.name), extCounts, depth + 1);
10574
+ walkForExtensions((0, import_node_path19.join)(dir, entry.name), extCounts, depth + 1);
10322
10575
  } else {
10323
- const ext = (0, import_node_path18.extname)(entry.name).toLowerCase();
10576
+ const ext = (0, import_node_path19.extname)(entry.name).toLowerCase();
10324
10577
  if (ext && EXTENSION_TO_LANGUAGE[ext]) {
10325
10578
  extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
10326
10579
  }
@@ -10361,13 +10614,13 @@ function detectLanguages(rootDir, supportedLanguages) {
10361
10614
  }
10362
10615
 
10363
10616
  // src/server/graph/core/audit-core.ts
10364
- var import_node_fs17 = require("node:fs");
10365
- var import_node_path19 = require("node:path");
10617
+ var import_node_fs18 = require("node:fs");
10618
+ var import_node_path20 = require("node:path");
10366
10619
  function readGraphFile(rootDir, layer) {
10367
- const filePath = (0, import_node_path19.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
10368
- if (!(0, import_node_fs17.existsSync)(filePath)) return null;
10620
+ const filePath = (0, import_node_path20.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
10621
+ if (!(0, import_node_fs18.existsSync)(filePath)) return null;
10369
10622
  try {
10370
- return JSON.parse((0, import_node_fs17.readFileSync)(filePath, "utf-8"));
10623
+ return JSON.parse((0, import_node_fs18.readFileSync)(filePath, "utf-8"));
10371
10624
  } catch {
10372
10625
  return null;
10373
10626
  }
@@ -10411,10 +10664,10 @@ function checkUnprotectedRoutes(rootDir) {
10411
10664
  const api = readGraphFile(rootDir, "api");
10412
10665
  const staticGraph = readGraphFile(rootDir, "static");
10413
10666
  if (!api) return buildReport("api", "unprotected_routes", findings);
10414
- const routePermsPath = (0, import_node_path19.join)(rootDir, "src", "config", "route-permissions.ts");
10667
+ const routePermsPath = (0, import_node_path20.join)(rootDir, "src", "config", "route-permissions.ts");
10415
10668
  let routePermsContent = "";
10416
- if ((0, import_node_fs17.existsSync)(routePermsPath)) {
10417
- routePermsContent = (0, import_node_fs17.readFileSync)(routePermsPath, "utf-8");
10669
+ if ((0, import_node_fs18.existsSync)(routePermsPath)) {
10670
+ routePermsContent = (0, import_node_fs18.readFileSync)(routePermsPath, "utf-8");
10418
10671
  }
10419
10672
  const registeredRoutes = /* @__PURE__ */ new Set();
10420
10673
  const routeEntryRe = /path:\s*'([^']+)'/g;
@@ -10491,10 +10744,10 @@ function checkUnenforcedPermissions(rootDir) {
10491
10744
  const staticGraph = readGraphFile(rootDir, "static");
10492
10745
  if (!staticGraph) return buildReport("static", "unenforced_permissions", findings);
10493
10746
  const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
10494
- const routePermsPath = (0, import_node_path19.join)(rootDir, "src", "config", "route-permissions.ts");
10747
+ const routePermsPath = (0, import_node_path20.join)(rootDir, "src", "config", "route-permissions.ts");
10495
10748
  let routePermsContent = "";
10496
- if ((0, import_node_fs17.existsSync)(routePermsPath)) {
10497
- routePermsContent = (0, import_node_fs17.readFileSync)(routePermsPath, "utf-8");
10749
+ if ((0, import_node_fs18.existsSync)(routePermsPath)) {
10750
+ routePermsContent = (0, import_node_fs18.readFileSync)(routePermsPath, "utf-8");
10498
10751
  }
10499
10752
  for (const perm of permissions) {
10500
10753
  const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
@@ -10524,9 +10777,9 @@ function checkHardcodedValues(rootDir) {
10524
10777
  const seen = /* @__PURE__ */ new Set();
10525
10778
  for (const node of api.nodes) {
10526
10779
  if (node.type !== "endpoint") continue;
10527
- const filePath = (0, import_node_path19.join)(rootDir, "src", node.id);
10528
- if (!(0, import_node_fs17.existsSync)(filePath)) continue;
10529
- const content = (0, import_node_fs17.readFileSync)(filePath, "utf-8");
10780
+ const filePath = (0, import_node_path20.join)(rootDir, "src", node.id);
10781
+ if (!(0, import_node_fs18.existsSync)(filePath)) continue;
10782
+ const content = (0, import_node_fs18.readFileSync)(filePath, "utf-8");
10530
10783
  let m;
10531
10784
  allCapsRe.lastIndex = 0;
10532
10785
  while ((m = allCapsRe.exec(content)) !== null) {
@@ -10925,6 +11178,81 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
10925
11178
  },
10926
11179
  required: ["node_id"]
10927
11180
  }
11181
+ },
11182
+ {
11183
+ name: "generate_blast_radius",
11184
+ description: `Generate a complete BlastRadiusManifest from graph data \u2014 ready to push to deck.
11185
+
11186
+ Two modes:
11187
+ - **Structural**: single node changed \u2192 auto-discover what's affected via reverse BFS
11188
+ Example: generate_blast_radius({ mode: "structural", node_id: "CommentChannel", title: "CommentChannel refactor" })
11189
+ - **Feature**: new feature \u2192 multiple starting nodes + new nodes to create
11190
+ Example: generate_blast_radius({ mode: "feature", title: "Client Role", description: "...", center_nodes: ["CommentChannel", "ProjectMember"], create_nodes: [{ id: "ChannelMember", name: "ChannelMember table", layer: "db", reason: "..." }] })
11191
+
11192
+ Output is a BlastRadiusManifest JSON that passes directly to the deck tool's blast-radius block.
11193
+ Reads ring/layer/center colors from .launchsecure/blast-radius-defaults.json.
11194
+ Auto-generates acceptance criteria per node using inspect_node AST data.`,
11195
+ inputSchema: {
11196
+ type: "object",
11197
+ properties: {
11198
+ mode: {
11199
+ type: "string",
11200
+ enum: ["structural", "feature"],
11201
+ description: '"structural" = single node changed. "feature" = new feature with multiple nodes.'
11202
+ },
11203
+ title: {
11204
+ type: "string",
11205
+ description: "Title for the blast radius (shown in center node and header)."
11206
+ },
11207
+ description: {
11208
+ type: "string",
11209
+ description: "Description of the change or feature."
11210
+ },
11211
+ subtitle: {
11212
+ type: "string",
11213
+ description: "Optional subtitle shown above title in the viz."
11214
+ },
11215
+ node_id: {
11216
+ type: "string",
11217
+ description: "Structural mode only: the node being changed."
11218
+ },
11219
+ center_nodes: {
11220
+ type: "array",
11221
+ items: { type: "string" },
11222
+ description: "Feature mode: existing graph node IDs that are the starting points for traversal."
11223
+ },
11224
+ create_nodes: {
11225
+ type: "array",
11226
+ items: {
11227
+ type: "object",
11228
+ properties: {
11229
+ id: { type: "string" },
11230
+ name: { type: "string" },
11231
+ layer: { type: "string" },
11232
+ type: { type: "string" },
11233
+ reason: { type: "string" },
11234
+ acceptance: { type: "array", items: { type: "string" } },
11235
+ connects_to: { type: "array", items: { type: "string" }, description: "IDs of existing nodes this new node has FK/relationship edges to." }
11236
+ },
11237
+ required: ["id", "name", "layer", "reason"]
11238
+ },
11239
+ description: "Feature mode: new nodes that need to be created (not in graph yet)."
11240
+ },
11241
+ hops: {
11242
+ type: "number",
11243
+ description: "Max hops for traversal. Default 2. Hop 1 = modify ring, hop 2+ = ripple ring."
11244
+ },
11245
+ push_to_deck: {
11246
+ type: "boolean",
11247
+ description: "If true, pushes the manifest directly to LaunchDeck browser (requires deck server running). Default false."
11248
+ },
11249
+ session: {
11250
+ type: "string",
11251
+ description: "Session name for the deck tab. Required when push_to_deck is true."
11252
+ }
11253
+ },
11254
+ required: ["title"]
11255
+ }
10928
11256
  }
10929
11257
  ];
10930
11258
  function matchesSearch(node, query) {
@@ -11201,6 +11529,144 @@ function handleBlastPoints(args) {
11201
11529
  }
11202
11530
  });
11203
11531
  }
11532
+ function handleGenerateBlastRadius(args) {
11533
+ const rootDir = process.cwd();
11534
+ const mode = args.mode ?? "structural";
11535
+ const title = args.title;
11536
+ const description = args.description ?? title;
11537
+ const subtitle = args.subtitle;
11538
+ const hops = args.hops ?? 2;
11539
+ const defaults = loadDefaults(rootDir);
11540
+ let centerNodeIds = [];
11541
+ if (mode === "structural") {
11542
+ const nodeId = args.node_id;
11543
+ if (!nodeId) return err("structural mode requires node_id");
11544
+ centerNodeIds = [nodeId];
11545
+ } else {
11546
+ centerNodeIds = args.center_nodes ?? [];
11547
+ if (centerNodeIds.length === 0) return err("feature mode requires center_nodes[]");
11548
+ }
11549
+ const createNodes = args.create_nodes ?? [];
11550
+ const blastResults = [];
11551
+ for (const nodeId of centerNodeIds) {
11552
+ let targetLayer;
11553
+ const graphs = readAllGraphs(rootDir);
11554
+ for (const [layer, graph2] of Object.entries(graphs)) {
11555
+ if (graph2 && graph2.nodes.some((n) => n.id === nodeId)) {
11556
+ targetLayer = layer;
11557
+ break;
11558
+ }
11559
+ }
11560
+ if (!targetLayer) continue;
11561
+ const graph = readGraph(rootDir, targetLayer);
11562
+ if (!graph) continue;
11563
+ const center = graph.nodes.find((n) => n.id === nodeId);
11564
+ if (!center) continue;
11565
+ const result2 = reverseNeighborhood(graph, nodeId, hops, "reverse");
11566
+ const affected = [];
11567
+ for (const [id, { node, hop }] of result2.nodes) {
11568
+ if (id === nodeId) continue;
11569
+ const tags = node.tags;
11570
+ affected.push({ id: node.id, name: node.name, type: node.type, layer: targetLayer, hop, module: tags?.module });
11571
+ }
11572
+ const otherLayers = getAvailableLayers(rootDir).filter((l) => l !== targetLayer && l !== "static");
11573
+ for (const otherLayer of otherLayers) {
11574
+ const otherGraph = readGraph(rootDir, otherLayer);
11575
+ if (!otherGraph) continue;
11576
+ for (const edge of otherGraph.edges) {
11577
+ if (edge.target === nodeId || edge.source === nodeId) {
11578
+ const dependentId = edge.target === nodeId ? edge.source : edge.target;
11579
+ if (affected.some((a) => a.id === dependentId)) continue;
11580
+ const depNode = otherGraph.nodes.find((n) => n.id === dependentId);
11581
+ if (depNode) {
11582
+ const tags = depNode.tags;
11583
+ affected.push({ id: depNode.id, name: depNode.name, type: depNode.type, layer: otherLayer, hop: 1, module: tags?.module });
11584
+ }
11585
+ }
11586
+ }
11587
+ }
11588
+ const centerTags = center.tags;
11589
+ const edges = result2.edges.map((e) => ({ source: e.source, target: e.target }));
11590
+ blastResults.push({
11591
+ center: { id: center.id, name: center.name, type: center.type, layer: targetLayer, module: centerTags?.module },
11592
+ affected,
11593
+ edges
11594
+ });
11595
+ }
11596
+ if (blastResults.length === 0) {
11597
+ return err(`None of the center nodes were found in any graph layer: ${centerNodeIds.join(", ")}`);
11598
+ }
11599
+ const inspectData = {};
11600
+ const allAffectedIds = /* @__PURE__ */ new Set();
11601
+ for (const r of blastResults) {
11602
+ allAffectedIds.add(r.center.id);
11603
+ for (const a of r.affected) allAffectedIds.add(a.id);
11604
+ }
11605
+ const allGraphs = readAllGraphs(rootDir);
11606
+ for (const id of allAffectedIds) {
11607
+ for (const [, graph] of Object.entries(allGraphs)) {
11608
+ if (!graph) continue;
11609
+ const node = graph.nodes.find((n) => n.id === id);
11610
+ if (node) {
11611
+ inspectData[id] = {
11612
+ type: node.type,
11613
+ name: node.name,
11614
+ methods: node.methods,
11615
+ path: node.path ?? node.handler,
11616
+ auth: node.auth,
11617
+ db_models: node.db_models
11618
+ };
11619
+ break;
11620
+ }
11621
+ }
11622
+ }
11623
+ const manifest = buildManifest({
11624
+ mode,
11625
+ title,
11626
+ description,
11627
+ subtitle,
11628
+ blastResults,
11629
+ createNodes,
11630
+ inspectData,
11631
+ defaults
11632
+ });
11633
+ const pushToDeck = args.push_to_deck;
11634
+ const session = args.session;
11635
+ let deckResult;
11636
+ if (pushToDeck) {
11637
+ if (!session) return err("push_to_deck requires a session name");
11638
+ const deckLockPath = (0, import_node_path21.join)(rootDir, ".launchsecure", "launch-deck.lock");
11639
+ if (!(0, import_node_fs19.existsSync)(deckLockPath)) {
11640
+ deckResult = { pushed: false, reason: "Deck server not running (no lock file). Push manually via deck tool." };
11641
+ } else {
11642
+ try {
11643
+ const lock = JSON.parse((0, import_node_fs19.readFileSync)(deckLockPath, "utf-8"));
11644
+ const deckUrl = lock.url;
11645
+ const body = JSON.stringify({
11646
+ session,
11647
+ mode: "show",
11648
+ blocks: [{ type: "blast-radius", label: title, manifest }]
11649
+ });
11650
+ (0, import_node_child_process2.execFileSync)("curl", [
11651
+ "-s",
11652
+ "-X",
11653
+ "POST",
11654
+ deckUrl + "/api/deck",
11655
+ "-H",
11656
+ "Content-Type: application/json",
11657
+ "-d",
11658
+ body
11659
+ ], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] });
11660
+ deckResult = { pushed: true, session, url: deckUrl };
11661
+ } catch (e) {
11662
+ deckResult = { pushed: false, reason: `Failed to push to deck: ${e}` };
11663
+ }
11664
+ }
11665
+ }
11666
+ const result = { ...manifest };
11667
+ if (deckResult) result._deck = deckResult;
11668
+ return okJson(result);
11669
+ }
11204
11670
  function layerSummary(graph) {
11205
11671
  const typeCounts = {};
11206
11672
  const moduleCounts = {};
@@ -11429,12 +11895,12 @@ function handleReadGraph(args) {
11429
11895
  return okJson(result);
11430
11896
  }
11431
11897
  function nodeToFilePath(rootDir, layer, nodeId) {
11432
- if (layer === "ui" || layer === "api") return (0, import_node_path20.join)(rootDir, "src", nodeId);
11433
- if (layer === "db") return (0, import_node_path20.join)(rootDir, "prisma", "schema.prisma");
11434
- const withSrc = (0, import_node_path20.join)(rootDir, "src", nodeId);
11435
- if ((0, import_node_fs18.existsSync)(withSrc)) return withSrc;
11436
- const direct = (0, import_node_path20.join)(rootDir, nodeId);
11437
- if ((0, import_node_fs18.existsSync)(direct)) return direct;
11898
+ if (layer === "ui" || layer === "api") return (0, import_node_path21.join)(rootDir, "src", nodeId);
11899
+ if (layer === "db") return (0, import_node_path21.join)(rootDir, "prisma", "schema.prisma");
11900
+ const withSrc = (0, import_node_path21.join)(rootDir, "src", nodeId);
11901
+ if ((0, import_node_fs19.existsSync)(withSrc)) return withSrc;
11902
+ const direct = (0, import_node_path21.join)(rootDir, nodeId);
11903
+ if ((0, import_node_fs19.existsSync)(direct)) return direct;
11438
11904
  return null;
11439
11905
  }
11440
11906
  function handleInspectNode(args) {
@@ -11577,11 +12043,11 @@ function handleGrepNodes(args) {
11577
12043
  let filesSearched = 0;
11578
12044
  let truncated = false;
11579
12045
  for (const [filePath, nodeId] of filePaths) {
11580
- if (!(0, import_node_fs18.existsSync)(filePath)) continue;
12046
+ if (!(0, import_node_fs19.existsSync)(filePath)) continue;
11581
12047
  filesSearched++;
11582
12048
  let content;
11583
12049
  try {
11584
- content = (0, import_node_fs18.readFileSync)(filePath, "utf-8");
12050
+ content = (0, import_node_fs19.readFileSync)(filePath, "utf-8");
11585
12051
  } catch {
11586
12052
  continue;
11587
12053
  }
@@ -11646,11 +12112,11 @@ function handleStartChartServer(args) {
11646
12112
  });
11647
12113
  }
11648
12114
  const entryPath = process.argv[1];
11649
- const logDir = (0, import_node_path20.join)((0, import_node_os2.homedir)(), ".launchsecure");
11650
- (0, import_node_fs18.mkdirSync)(logDir, { recursive: true });
11651
- const logPath = (0, import_node_path20.join)(logDir, "launch-chart.log");
11652
- const out = (0, import_node_fs18.openSync)(logPath, "a");
11653
- const err2 = (0, import_node_fs18.openSync)(logPath, "a");
12115
+ const logDir = (0, import_node_path21.join)((0, import_node_os2.homedir)(), ".launchsecure");
12116
+ (0, import_node_fs19.mkdirSync)(logDir, { recursive: true });
12117
+ const logPath = (0, import_node_path21.join)(logDir, "launch-chart.log");
12118
+ const out = (0, import_node_fs19.openSync)(logPath, "a");
12119
+ const err2 = (0, import_node_fs19.openSync)(logPath, "a");
11654
12120
  const portArgs = args.port ? ["--port", String(args.port)] : [];
11655
12121
  const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
11656
12122
  detached: true,
@@ -11770,20 +12236,20 @@ function handleDetectProjectStack() {
11770
12236
  if (ref.type === "references_api") stats.references_api++;
11771
12237
  }
11772
12238
  }
11773
- const srcDir = (0, import_node_path20.join)(rootDir, "src");
11774
- if ((0, import_node_fs18.existsSync)(srcDir)) {
12239
+ const srcDir = (0, import_node_path21.join)(rootDir, "src");
12240
+ if ((0, import_node_fs19.existsSync)(srcDir)) {
11775
12241
  const scanDir = (dir) => {
11776
- if (!(0, import_node_fs18.existsSync)(dir)) return;
11777
- for (const entry of (0, import_node_fs18.readdirSync)(dir, { withFileTypes: true })) {
12242
+ if (!(0, import_node_fs19.existsSync)(dir)) return;
12243
+ for (const entry of (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true })) {
11778
12244
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
11779
- const full = (0, import_node_path20.join)(dir, entry.name);
12245
+ const full = (0, import_node_path21.join)(dir, entry.name);
11780
12246
  if (entry.isDirectory()) {
11781
12247
  scanDir(full);
11782
12248
  continue;
11783
12249
  }
11784
- if (![".ts", ".tsx"].includes((0, import_node_path20.extname)(entry.name))) continue;
12250
+ if (![".ts", ".tsx"].includes((0, import_node_path21.extname)(entry.name))) continue;
11785
12251
  try {
11786
- const content = (0, import_node_fs18.readFileSync)(full, "utf-8");
12252
+ const content = (0, import_node_fs19.readFileSync)(full, "utf-8");
11787
12253
  const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
11788
12254
  if (matches) stats.annotations += matches.length;
11789
12255
  } catch {
@@ -11902,6 +12368,10 @@ async function handleMessage(msg) {
11902
12368
  respond(id ?? null, handleBlastPoints(args));
11903
12369
  return;
11904
12370
  }
12371
+ if (toolName === "generate_blast_radius") {
12372
+ respond(id ?? null, handleGenerateBlastRadius(args));
12373
+ return;
12374
+ }
11905
12375
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
11906
12376
  return;
11907
12377
  }
@@ -11953,7 +12423,8 @@ if (import_fs8.default.existsSync(envPath)) {
11953
12423
  }
11954
12424
  }
11955
12425
  var LAUNCHSECURE_URL = process.env.LAUNCHSECURE_URL || "http://localhost:4177";
11956
- var CONFIG_FILE = "launchpod.config.json";
12426
+ var CONFIG_FILE = ".launchpod.json";
12427
+ var CONFIG_FILE_LEGACY = "launchpod.config.json";
11957
12428
  var REPO_ROOT = process.cwd();
11958
12429
  var PROJECT_DIR = REPO_ROOT;
11959
12430
  var CREDENTIALS_DIR = import_path8.default.join(PROJECT_DIR, ".launchpod");
@@ -11989,8 +12460,10 @@ function parseArgs() {
11989
12460
  port = parseInt(process.env.PORT, 10);
11990
12461
  } else {
11991
12462
  const configPath = import_path8.default.join(REPO_ROOT, CONFIG_FILE);
11992
- if (import_fs8.default.existsSync(configPath)) {
11993
- const config = JSON.parse(import_fs8.default.readFileSync(configPath, "utf-8"));
12463
+ const legacyPath = import_path8.default.join(REPO_ROOT, CONFIG_FILE_LEGACY);
12464
+ const resolvedPath = import_fs8.default.existsSync(configPath) ? configPath : import_fs8.default.existsSync(legacyPath) ? legacyPath : null;
12465
+ if (resolvedPath) {
12466
+ const config = JSON.parse(import_fs8.default.readFileSync(resolvedPath, "utf-8"));
11994
12467
  if (config.port) port = config.port;
11995
12468
  }
11996
12469
  }
@@ -12219,21 +12692,21 @@ function ensureGitRepo(runId) {
12219
12692
  console.log("Git repo initialized");
12220
12693
  }
12221
12694
  const branch = `pipeline/${runId}`;
12222
- try {
12223
- const currentBranch = (0, import_child_process4.execSync)("git rev-parse --abbrev-ref HEAD", { cwd: REPO_ROOT, stdio: "pipe" }).toString().trim();
12224
- if (currentBranch === branch) {
12225
- console.log(`Already on branch ${branch}`);
12226
- return;
12227
- }
12228
- } catch {
12695
+ const worktreeDir = import_path8.default.join(REPO_ROOT, ".launchpod", "worktrees", runId);
12696
+ if (import_fs8.default.existsSync(worktreeDir)) {
12697
+ PROJECT_DIR = worktreeDir;
12698
+ console.log(`Resuming in existing worktree ${worktreeDir}`);
12699
+ return;
12229
12700
  }
12701
+ import_fs8.default.mkdirSync(import_path8.default.join(REPO_ROOT, ".launchpod", "worktrees"), { recursive: true });
12230
12702
  try {
12231
- (0, import_child_process4.execSync)(`git checkout "${branch}"`, { cwd: REPO_ROOT, stdio: "pipe" });
12232
- console.log(`Checked out existing branch ${branch}`);
12703
+ (0, import_child_process4.execSync)(`git worktree add "${worktreeDir}" "${branch}"`, { cwd: REPO_ROOT, stdio: "pipe" });
12704
+ console.log(`Created worktree from existing branch ${branch}`);
12233
12705
  } catch {
12234
- (0, import_child_process4.execSync)(`git checkout -b "${branch}"`, { cwd: REPO_ROOT, stdio: "pipe" });
12235
- console.log(`Created branch ${branch}`);
12706
+ (0, import_child_process4.execSync)(`git worktree add -b "${branch}" "${worktreeDir}"`, { cwd: REPO_ROOT, stdio: "pipe" });
12707
+ console.log(`Created worktree with new branch ${branch}`);
12236
12708
  }
12709
+ PROJECT_DIR = worktreeDir;
12237
12710
  }
12238
12711
  function normalizeGitUrl(url) {
12239
12712
  return url.replace(/\.git$/, "").replace(/^git@github\.com:/, "https://github.com/").replace(/\/$/, "").toLowerCase();
@@ -12336,7 +12809,8 @@ function parseTrackerData(result) {
12336
12809
  const runStatus = typeof run?.status === "string" ? run.status : void 0;
12337
12810
  return { agents, interventions, runStatus };
12338
12811
  }
12339
- async function startPipelineSystem(creds) {
12812
+ async function startPipelineSystem(creds, capabilities) {
12813
+ const activeCapabilities = capabilities ?? { ...DEFAULT_CAPABILITIES };
12340
12814
  try {
12341
12815
  writeMcpConfigs({
12342
12816
  projectDir: PROJECT_DIR,
@@ -12602,7 +13076,7 @@ if (!__isMcpMode) {
12602
13076
  });
12603
13077
  authenticated = true;
12604
13078
  const savedCreds = loadCredentials();
12605
- if (savedCreds) startPipelineSystem(savedCreds);
13079
+ if (savedCreds) startPipelineSystem(savedCreds, activeCapabilities);
12606
13080
  res.writeHead(200, { "Content-Type": "application/json" });
12607
13081
  res.end(JSON.stringify({ success: true }));
12608
13082
  } catch {
@@ -12820,7 +13294,7 @@ if (!__isMcpMode) {
12820
13294
  }
12821
13295
  serveIndex(res);
12822
13296
  });
12823
- let activeCapabilities2 = { ...DEFAULT_CAPABILITIES };
13297
+ let activeCapabilities = { ...DEFAULT_CAPABILITIES };
12824
13298
  async function main() {
12825
13299
  const { token, serverUrl } = parsedArgs;
12826
13300
  if (token) {
@@ -12837,8 +13311,8 @@ if (!__isMcpMode) {
12837
13311
  process.exit(1);
12838
13312
  }
12839
13313
  activeRunId = runId;
12840
- activeCapabilities2 = await fetchCapabilities(serverUrl, token);
12841
- console.log(`Capabilities: workspace_setup=${activeCapabilities2.workspace_setup}, terminal=${activeCapabilities2.terminal}`);
13314
+ activeCapabilities = await fetchCapabilities(serverUrl, token);
13315
+ console.log(`Capabilities: workspace_setup=${activeCapabilities.workspace_setup}, terminal=${activeCapabilities.terminal}`);
12842
13316
  ensureGitRepo(runId);
12843
13317
  await validateRepoUrl(serverUrl, token);
12844
13318
  const lock = checkLockfile();
@@ -12866,13 +13340,13 @@ if (!__isMcpMode) {
12866
13340
  const existingCreds = loadCredentials();
12867
13341
  if (existingCreds) {
12868
13342
  if (!token) {
12869
- activeCapabilities2 = await fetchCapabilities(existingCreds.serverUrl, existingCreds.token);
13343
+ activeCapabilities = await fetchCapabilities(existingCreds.serverUrl, existingCreds.token);
12870
13344
  }
12871
- if (activeCapabilities2.terminal) {
13345
+ if (activeCapabilities.terminal) {
12872
13346
  initTerminalBridge(server, PROJECT_DIR, import_path8.default.join(PROJECT_DIR, ".launchpod", "sessions"));
12873
13347
  }
12874
- startPipelineSystem(existingCreds);
12875
- } else if (activeCapabilities2.terminal) {
13348
+ startPipelineSystem(existingCreds, activeCapabilities);
13349
+ } else if (activeCapabilities.terminal) {
12876
13350
  initTerminalBridge(server, PROJECT_DIR, import_path8.default.join(PROJECT_DIR, ".launchpod", "sessions"));
12877
13351
  }
12878
13352
  }