@launchsecure/launch-kit 0.0.5 → 0.0.6

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.
@@ -1727,7 +1727,7 @@ var require_usage_reader = __commonJS({
1727
1727
  async readJsonlFile(filePath, cutoffTime) {
1728
1728
  const entries = [];
1729
1729
  const fileProcessedEntries = /* @__PURE__ */ new Set();
1730
- return new Promise((resolve2) => {
1730
+ return new Promise((resolve3) => {
1731
1731
  const rl = readline.createInterface({
1732
1732
  input: createReadStream(filePath),
1733
1733
  crlfDelay: Infinity
@@ -1785,11 +1785,11 @@ var require_usage_reader = __commonJS({
1785
1785
  }
1786
1786
  });
1787
1787
  rl.on("close", () => {
1788
- resolve2(entries);
1788
+ resolve3(entries);
1789
1789
  });
1790
1790
  rl.on("error", (error) => {
1791
1791
  console.error("Error reading file:", filePath, error);
1792
- resolve2(entries);
1792
+ resolve3(entries);
1793
1793
  });
1794
1794
  });
1795
1795
  }
@@ -3587,7 +3587,7 @@ var require_src = __commonJS({
3587
3587
  if (session.active) throw new Error(`Agent already running in session ${sessionId}`);
3588
3588
  const { command, args = [], env = {} } = options;
3589
3589
  if (!command) throw new Error("startScriptInSession requires a command");
3590
- return new Promise((resolve2, reject) => {
3590
+ return new Promise((resolve3, reject) => {
3591
3591
  this.scriptBridge.startSession(sessionId, {
3592
3592
  command,
3593
3593
  args,
@@ -3609,7 +3609,7 @@ var require_src = __commonJS({
3609
3609
  session.lastActivity = /* @__PURE__ */ new Date();
3610
3610
  this.broadcastToSession(sessionId, { type: "script_stopped", sessionId });
3611
3611
  if (exitCode === 0) {
3612
- resolve2({ code: exitCode, signal });
3612
+ resolve3({ code: exitCode, signal });
3613
3613
  } else {
3614
3614
  reject(new Error(`Script exited with code ${exitCode}`));
3615
3615
  }
@@ -3701,6 +3701,30 @@ var require_src = __commonJS({
3701
3701
  }
3702
3702
  });
3703
3703
 
3704
+ // src/server/graph/core/config.ts
3705
+ var config_exports = {};
3706
+ __export(config_exports, {
3707
+ loadConfig: () => loadConfig
3708
+ });
3709
+ function loadConfig(rootDir) {
3710
+ const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
3711
+ if (!(0, import_node_fs.existsSync)(configPath)) return {};
3712
+ try {
3713
+ return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
3714
+ } catch {
3715
+ return {};
3716
+ }
3717
+ }
3718
+ var import_node_fs, import_node_path, CONFIG_FILENAME;
3719
+ var init_config = __esm({
3720
+ "src/server/graph/core/config.ts"() {
3721
+ "use strict";
3722
+ import_node_fs = require("node:fs");
3723
+ import_node_path = require("node:path");
3724
+ CONFIG_FILENAME = ".launchchart.json";
3725
+ }
3726
+ });
3727
+
3704
3728
  // src/server/cli.ts
3705
3729
  var import_http = __toESM(require("http"));
3706
3730
  var import_https = __toESM(require("https"));
@@ -5270,7 +5294,7 @@ var PostImplLaunchExecutor = class {
5270
5294
  return 3001;
5271
5295
  }
5272
5296
  startDevServer(port, databaseUrl) {
5273
- return new Promise((resolve2) => {
5297
+ return new Promise((resolve3) => {
5274
5298
  const env = { ...process.env, PORT: String(port), ...databaseUrl ? { DATABASE_URL: databaseUrl } : {} };
5275
5299
  this.devProcess = (0, import_child_process3.spawn)("npm", ["run", "dev"], {
5276
5300
  cwd: this.workingDir,
@@ -5282,7 +5306,7 @@ var PostImplLaunchExecutor = class {
5282
5306
  const timeout = setTimeout(() => {
5283
5307
  if (!resolved) {
5284
5308
  resolved = true;
5285
- this.healthCheck(port).then(resolve2);
5309
+ this.healthCheck(port).then(resolve3);
5286
5310
  }
5287
5311
  }, 15e3);
5288
5312
  const onData = (data) => {
@@ -5291,7 +5315,7 @@ var PostImplLaunchExecutor = class {
5291
5315
  if (!resolved) {
5292
5316
  resolved = true;
5293
5317
  clearTimeout(timeout);
5294
- resolve2(true);
5318
+ resolve3(true);
5295
5319
  }
5296
5320
  }
5297
5321
  };
@@ -5302,7 +5326,7 @@ var PostImplLaunchExecutor = class {
5302
5326
  if (!resolved) {
5303
5327
  resolved = true;
5304
5328
  clearTimeout(timeout);
5305
- resolve2(false);
5329
+ resolve3(false);
5306
5330
  }
5307
5331
  });
5308
5332
  this.devProcess.unref();
@@ -6324,26 +6348,13 @@ ${links}
6324
6348
  }
6325
6349
 
6326
6350
  // src/server/graph/index.ts
6327
- var import_node_fs9 = require("node:fs");
6328
- var import_node_path10 = require("node:path");
6351
+ var import_node_fs11 = require("node:fs");
6352
+ var import_node_path13 = require("node:path");
6329
6353
 
6330
6354
  // src/server/graph/core/graph-builder.ts
6331
6355
  var import_node_fs8 = require("node:fs");
6332
6356
  var import_node_path9 = require("node:path");
6333
-
6334
- // src/server/graph/core/config.ts
6335
- var import_node_fs = require("node:fs");
6336
- var import_node_path = require("node:path");
6337
- var CONFIG_FILENAME = ".launchchart.json";
6338
- function loadConfig(rootDir) {
6339
- const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
6340
- if (!(0, import_node_fs.existsSync)(configPath)) return {};
6341
- try {
6342
- return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
6343
- } catch {
6344
- return {};
6345
- }
6346
- }
6357
+ init_config();
6347
6358
 
6348
6359
  // src/server/graph/core/parser-registry.ts
6349
6360
  var import_node_path8 = require("node:path");
@@ -6809,34 +6820,6 @@ function classifyType(id) {
6809
6820
  if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
6810
6821
  return "component";
6811
6822
  }
6812
- function classifyModule(id) {
6813
- if (/app\/\(auth\)\//.test(id)) return "auth";
6814
- if (/app\/\(admin\)\//.test(id)) return "admin";
6815
- if (/app\/\(settings\)\//.test(id)) return "settings";
6816
- if (/app\/\(app\)\/\[orgSlug\]\/\(project-pages\)\//.test(id)) return "project";
6817
- if (/app\/\(app\)\/\[orgSlug\]\/\(org-pages\)\//.test(id)) return "org";
6818
- if (/app\/\(app\)\/\[orgSlug\]\//.test(id)) return "org";
6819
- if (id.startsWith("app/integrations/")) return "integrations";
6820
- if (id.startsWith("app/docs/")) return "admin";
6821
- if (id.startsWith("client/components/ui/")) return "shared-ui";
6822
- if (id.startsWith("client/components/layout/") || /client\/lib\/navigation/.test(id)) return "layout";
6823
- if (/client\/components\/auth\//.test(id) || /client\/lib\/auth-/.test(id) || /client\/lib\/github-oauth/.test(id) || /client\/lib\/permission-service/.test(id) || /client\/hooks\/use-permissions/.test(id)) return "auth";
6824
- if (/client\/components\/prd-/.test(id) || /client\/hooks\/use-admin/.test(id)) return "admin";
6825
- if (/client\/components\/org-/.test(id) || /client\/hooks\/use-org-/.test(id) || /client\/hooks\/use-provider-def/.test(id)) return "org";
6826
- if (/client\/components\/project/.test(id) || /client\/hooks\/use-project-/.test(id) || /client\/hooks\/use-pipeline/.test(id) || /client\/hooks\/use-databases/.test(id) || /client\/hooks\/use-provider-env/.test(id) || /client\/hooks\/use-role-assign/.test(id) || /client\/components\/pipeline/.test(id) || /client\/components\/deployments/.test(id)) return "project";
6827
- if (/client\/hooks\/use-(profile|sessions|organizations|notification)/.test(id)) return "settings";
6828
- if (id.startsWith("server/auth/")) return "auth";
6829
- if (id.startsWith("server/mcp/")) return "mcp";
6830
- if (id.startsWith("server/lib/")) return "server-lib";
6831
- if (id.startsWith("server/middleware")) return "middleware";
6832
- if (id.startsWith("server/services/")) return "services";
6833
- if (id.startsWith("server/db")) return "db";
6834
- if (id.startsWith("server/errors")) return "errors";
6835
- if (id.startsWith("server/")) return "server-lib";
6836
- if (id.startsWith("config/")) return "config";
6837
- if (id.startsWith("lib/")) return "lib";
6838
- return "root";
6839
- }
6840
6823
  function extractRoute(id) {
6841
6824
  if (!id.endsWith("/page.tsx")) return null;
6842
6825
  let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
@@ -7057,8 +7040,7 @@ function generate(rootDir) {
7057
7040
  const parsed = parsedByPath.get(absPath);
7058
7041
  const name = parsed.name || nameFromFilename(absPath);
7059
7042
  const route = extractRoute(id);
7060
- const module_ = classifyModule(id);
7061
- nodes.push({ id, type, name, route, module: module_, exports: parsed.exports });
7043
+ nodes.push({ id, type, name, route, exports: parsed.exports });
7062
7044
  nodeIdSet.add(id);
7063
7045
  nodeTypeMap.set(id, type);
7064
7046
  if (route) routeToNodeId.set(route, id);
@@ -7162,7 +7144,6 @@ function generate(rootDir) {
7162
7144
  type: "external",
7163
7145
  name: parsed.name || nameFromFilename(absPath),
7164
7146
  route: null,
7165
- module: "external",
7166
7147
  exports: parsed.exports
7167
7148
  });
7168
7149
  nodeIdSet.add(externalId);
@@ -8177,31 +8158,403 @@ function generateAll(rootDir) {
8177
8158
  }
8178
8159
 
8179
8160
  // src/server/graph/index.ts
8161
+ init_config();
8162
+
8163
+ // src/server/graph/core/tagger-registry.ts
8164
+ var import_node_path11 = require("node:path");
8165
+
8166
+ // src/server/graph/taggers/module-tagger.ts
8167
+ var import_node_fs9 = require("node:fs");
8168
+ var import_node_path10 = require("node:path");
8169
+ function matchGlob(pattern, id) {
8170
+ const patParts = pattern.split("/");
8171
+ const idParts = id.split("/");
8172
+ return matchParts(patParts, 0, idParts, 0);
8173
+ }
8174
+ function matchParts(pat, pi, id, ii) {
8175
+ while (pi < pat.length && ii < id.length) {
8176
+ const p = pat[pi];
8177
+ if (p === "**") {
8178
+ for (let skip = ii; skip <= id.length; skip++) {
8179
+ if (matchParts(pat, pi + 1, id, skip)) return true;
8180
+ }
8181
+ return false;
8182
+ }
8183
+ if (p === "*") {
8184
+ pi++;
8185
+ ii++;
8186
+ continue;
8187
+ }
8188
+ if (p !== id[ii]) return false;
8189
+ pi++;
8190
+ ii++;
8191
+ }
8192
+ while (pi < pat.length && pat[pi] === "**") pi++;
8193
+ return pi === pat.length && ii === id.length;
8194
+ }
8195
+ var CONVENTION_DIRS = ["features", "modules", "domains", "areas"];
8196
+ function detectConventionDirs(rootDir) {
8197
+ const result = /* @__PURE__ */ new Map();
8198
+ const searchDirs = [
8199
+ rootDir,
8200
+ (0, import_node_path10.join)(rootDir, "src"),
8201
+ (0, import_node_path10.join)(rootDir, "app"),
8202
+ (0, import_node_path10.join)(rootDir, "lib")
8203
+ ];
8204
+ for (const base of searchDirs) {
8205
+ for (const convention of CONVENTION_DIRS) {
8206
+ const dir = (0, import_node_path10.join)(base, convention);
8207
+ if (!(0, import_node_fs9.existsSync)(dir)) continue;
8208
+ try {
8209
+ const stat = (0, import_node_fs9.statSync)(dir);
8210
+ if (!stat.isDirectory()) continue;
8211
+ const entries = (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
8212
+ if (entries.length > 0) {
8213
+ const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
8214
+ result.set(relPath, entries);
8215
+ }
8216
+ } catch {
8217
+ }
8218
+ }
8219
+ }
8220
+ return result;
8221
+ }
8222
+ function extractRouteGroups(id) {
8223
+ const groups = [];
8224
+ const re = /\(([^)]+)\)/g;
8225
+ let m;
8226
+ while ((m = re.exec(id)) !== null) {
8227
+ groups.push(m[1]);
8228
+ }
8229
+ return groups;
8230
+ }
8231
+ var SKIP_SEGMENTS = /* @__PURE__ */ new Set([
8232
+ "src",
8233
+ "app",
8234
+ "client",
8235
+ "server",
8236
+ "lib",
8237
+ "config"
8238
+ ]);
8239
+ function isRouteGroup(segment) {
8240
+ return segment.startsWith("(") && segment.endsWith(")");
8241
+ }
8242
+ function isDynamicSegment(segment) {
8243
+ return segment.startsWith("[") || segment.startsWith(":");
8244
+ }
8245
+ function isDomainDir(segment) {
8246
+ return segment.includes(".") && !segment.endsWith(".tsx") && !segment.endsWith(".ts") && !segment.endsWith(".js") && !segment.endsWith(".jsx") && !segment.endsWith(".vue");
8247
+ }
8248
+ var TRIVIAL_GROUPS = /* @__PURE__ */ new Set([
8249
+ "app",
8250
+ "all",
8251
+ "ee",
8252
+ "home",
8253
+ "root"
8254
+ ]);
8255
+ function isTrivialGroup(name) {
8256
+ if (TRIVIAL_GROUPS.has(name)) return true;
8257
+ const lower = name.toLowerCase();
8258
+ const wrapperPatterns = [
8259
+ /^.*-?wrapper$/,
8260
+ // "page-wrapper", "use-page-wrapper"
8261
+ /^.*-?layout$/,
8262
+ // "admin-layout", "settings-layout"
8263
+ /^use-/,
8264
+ // "use-page-wrapper"
8265
+ /^default$/
8266
+ ];
8267
+ return wrapperPatterns.some((p) => p.test(lower));
8268
+ }
8269
+ function normalizeGroupName(name) {
8270
+ return name.replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
8271
+ }
8272
+ function extractModuleFromPath(id) {
8273
+ const segments = id.split("/");
8274
+ const routeGroups = extractRouteGroups(id);
8275
+ const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g)).map(normalizeGroupName);
8276
+ if (moduleGroups.length > 0) {
8277
+ return moduleGroups[moduleGroups.length - 1];
8278
+ }
8279
+ const meaningful = [];
8280
+ for (const seg of segments) {
8281
+ if (seg.includes(".")) continue;
8282
+ if (isRouteGroup(seg)) continue;
8283
+ if (isDynamicSegment(seg)) continue;
8284
+ if (isDomainDir(seg)) continue;
8285
+ if (SKIP_SEGMENTS.has(seg)) continue;
8286
+ meaningful.push(seg);
8287
+ }
8288
+ if (meaningful.length > 0) {
8289
+ return meaningful[0];
8290
+ }
8291
+ return "root";
8292
+ }
8293
+ var cachedRootDir = null;
8294
+ var cachedConventionDirs = /* @__PURE__ */ new Map();
8295
+ var moduleTagger = {
8296
+ id: "module",
8297
+ tagKey: "module",
8298
+ trackUntagged: true,
8299
+ layers: null,
8300
+ // applies to all layers
8301
+ tag(nodes, layer, rootDir) {
8302
+ if (cachedRootDir !== rootDir) {
8303
+ cachedConventionDirs = detectConventionDirs(rootDir);
8304
+ cachedRootDir = rootDir;
8305
+ }
8306
+ let configRules = [];
8307
+ try {
8308
+ const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
8309
+ const config = loadConfig2(rootDir);
8310
+ configRules = config.taggers?.module?.rules ?? [];
8311
+ } catch {
8312
+ }
8313
+ const result = /* @__PURE__ */ new Map();
8314
+ for (const node of nodes) {
8315
+ const id = node.id;
8316
+ let matched = false;
8317
+ for (const rule of configRules) {
8318
+ if (matchGlob(rule.match, id)) {
8319
+ result.set(id, rule.module);
8320
+ matched = true;
8321
+ break;
8322
+ }
8323
+ }
8324
+ if (matched) continue;
8325
+ matched = false;
8326
+ for (const [convDir, moduleNames] of cachedConventionDirs) {
8327
+ if (id.startsWith(convDir + "/")) {
8328
+ const rest = id.slice(convDir.length + 1);
8329
+ const firstSeg = rest.split("/")[0];
8330
+ if (moduleNames.includes(firstSeg)) {
8331
+ result.set(id, firstSeg);
8332
+ matched = true;
8333
+ break;
8334
+ }
8335
+ }
8336
+ }
8337
+ if (matched) continue;
8338
+ const module2 = extractModuleFromPath(id);
8339
+ result.set(id, module2);
8340
+ }
8341
+ return result;
8342
+ }
8343
+ };
8344
+
8345
+ // src/server/graph/taggers/screen-tagger.ts
8346
+ var SCREEN_TYPES = /* @__PURE__ */ new Set(["page", "layout"]);
8347
+ var screenTagger = {
8348
+ id: "screen",
8349
+ tagKey: "screen",
8350
+ trackUntagged: true,
8351
+ layers: ["ui"],
8352
+ tag(nodes, layer) {
8353
+ if (layer !== "ui") return /* @__PURE__ */ new Map();
8354
+ const result = /* @__PURE__ */ new Map();
8355
+ for (const node of nodes) {
8356
+ if (SCREEN_TYPES.has(node.type)) {
8357
+ result.set(node.id, "true");
8358
+ }
8359
+ }
8360
+ return result;
8361
+ }
8362
+ };
8363
+
8364
+ // src/server/graph/core/tagger-registry.ts
8365
+ var TaggerRegistry = class {
8366
+ constructor() {
8367
+ this.taggers = [];
8368
+ this.ids = /* @__PURE__ */ new Set();
8369
+ }
8370
+ register(tagger) {
8371
+ if (this.ids.has(tagger.id)) {
8372
+ throw new Error(`Duplicate tagger id: ${tagger.id}`);
8373
+ }
8374
+ this.ids.add(tagger.id);
8375
+ this.taggers.push(tagger);
8376
+ }
8377
+ getAll() {
8378
+ return this.taggers;
8379
+ }
8380
+ getForLayer(layer) {
8381
+ return this.taggers.filter((t) => t.layers === null || t.layers.includes(layer));
8382
+ }
8383
+ };
8384
+ var BUILTIN_TAGGERS = [moduleTagger, screenTagger];
8385
+ function registerBuiltins2(registry, disabled, config) {
8386
+ for (const tagger of BUILTIN_TAGGERS) {
8387
+ if (disabled.has(tagger.id)) continue;
8388
+ const override = config.taggers?.trackUntagged?.[tagger.id];
8389
+ if (override !== void 0) {
8390
+ tagger.trackUntagged = override;
8391
+ }
8392
+ registry.register(tagger);
8393
+ }
8394
+ }
8395
+ function loadCustomTaggers(registry, config, rootDir, disabled) {
8396
+ for (const entry of config.taggers?.custom ?? []) {
8397
+ if (disabled.has(entry.id)) continue;
8398
+ try {
8399
+ const absPath = (0, import_node_path11.resolve)(rootDir, entry.path);
8400
+ const mod = require(absPath);
8401
+ const tagger = "default" in mod ? mod.default : mod;
8402
+ const override = config.taggers?.trackUntagged?.[tagger.id];
8403
+ if (override !== void 0) {
8404
+ tagger.trackUntagged = override;
8405
+ }
8406
+ registry.register(tagger);
8407
+ } catch (err2) {
8408
+ process.stderr.write(`[launch-chart] failed to load custom tagger from ${entry.path}: ${err2}
8409
+ `);
8410
+ }
8411
+ }
8412
+ }
8413
+ function createTaggerRegistry(config, rootDir) {
8414
+ const registry = new TaggerRegistry();
8415
+ const disabled = new Set(config.taggers?.disabled ?? []);
8416
+ registerBuiltins2(registry, disabled, config);
8417
+ loadCustomTaggers(registry, config, rootDir, disabled);
8418
+ return registry;
8419
+ }
8420
+
8421
+ // src/server/graph/core/tag-store.ts
8422
+ var import_node_fs10 = require("node:fs");
8423
+ var import_node_path12 = require("node:path");
8424
+ var TAGS_FILENAME = "tags.json";
8180
8425
  var GRAPHS_DIR = ".launchsecure/graphs";
8426
+ var tagCache = /* @__PURE__ */ new Map();
8427
+ function tagsFilePath(rootDir) {
8428
+ return (0, import_node_path12.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
8429
+ }
8430
+ function readTagStore(rootDir) {
8431
+ const filePath = tagsFilePath(rootDir);
8432
+ if (!(0, import_node_fs10.existsSync)(filePath)) return {};
8433
+ const stat = (0, import_node_fs10.statSync)(filePath);
8434
+ const cached = tagCache.get(filePath);
8435
+ if (cached && cached.mtimeMs === stat.mtimeMs) {
8436
+ return cached.store;
8437
+ }
8438
+ try {
8439
+ const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
8440
+ const store = JSON.parse(content);
8441
+ tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
8442
+ return store;
8443
+ } catch {
8444
+ return {};
8445
+ }
8446
+ }
8447
+ function writeTagStore(rootDir, store) {
8448
+ const filePath = tagsFilePath(rootDir);
8449
+ const dir = (0, import_node_path12.dirname)(filePath);
8450
+ (0, import_node_fs10.mkdirSync)(dir, { recursive: true });
8451
+ const cleaned = {};
8452
+ for (const [nodeId, tags] of Object.entries(store)) {
8453
+ if (Object.keys(tags).length > 0) {
8454
+ cleaned[nodeId] = tags;
8455
+ }
8456
+ }
8457
+ (0, import_node_fs10.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
8458
+ tagCache.delete(filePath);
8459
+ }
8460
+ function setTag(rootDir, nodeId, key, value) {
8461
+ const store = readTagStore(rootDir);
8462
+ if (!store[nodeId]) store[nodeId] = {};
8463
+ store[nodeId][key] = value;
8464
+ writeTagStore(rootDir, store);
8465
+ }
8466
+ function removeTag(rootDir, nodeId, key) {
8467
+ const store = readTagStore(rootDir);
8468
+ if (!store[nodeId]) return;
8469
+ delete store[nodeId][key];
8470
+ if (Object.keys(store[nodeId]).length === 0) {
8471
+ delete store[nodeId];
8472
+ }
8473
+ writeTagStore(rootDir, store);
8474
+ }
8475
+
8476
+ // src/server/graph/index.ts
8477
+ var GRAPHS_DIR2 = ".launchsecure/graphs";
8181
8478
  var LAYERS = ["ui", "api", "db"];
8182
8479
  var graphCache = /* @__PURE__ */ new Map();
8480
+ var taggedCache = /* @__PURE__ */ new Map();
8183
8481
  function graphsDir(rootDir) {
8184
- return (0, import_node_path10.join)(rootDir, GRAPHS_DIR);
8482
+ return (0, import_node_path13.join)(rootDir, GRAPHS_DIR2);
8185
8483
  }
8186
8484
  function graphFilePath(rootDir, layer) {
8187
- return (0, import_node_path10.join)(graphsDir(rootDir), `${layer}.json`);
8485
+ return (0, import_node_path13.join)(graphsDir(rootDir), `${layer}.json`);
8486
+ }
8487
+ function tagsFilePath2(rootDir) {
8488
+ return (0, import_node_path13.join)(graphsDir(rootDir), "tags.json");
8489
+ }
8490
+ function getMtimeMs(filePath) {
8491
+ if (!(0, import_node_fs11.existsSync)(filePath)) return 0;
8492
+ return (0, import_node_fs11.statSync)(filePath).mtimeMs;
8188
8493
  }
8189
8494
  function invalidateCache(filePath) {
8190
8495
  graphCache.delete(filePath);
8191
8496
  }
8192
- function readGraph(rootDir, layer) {
8497
+ function invalidateTaggedCache(rootDir, layer) {
8498
+ taggedCache.delete(`${rootDir}:${layer}`);
8499
+ }
8500
+ function applyTags(graph, layer, rootDir) {
8501
+ const config = loadConfig(rootDir);
8502
+ const registry = createTaggerRegistry(config, rootDir);
8503
+ const manualTags = readTagStore(rootDir);
8504
+ const taggedNodes = graph.nodes.map((n) => ({ ...n }));
8505
+ const taggers = registry.getForLayer(layer);
8506
+ for (const tagger of taggers) {
8507
+ const assignments = tagger.tag(taggedNodes, layer, rootDir);
8508
+ for (const node of taggedNodes) {
8509
+ if (!node.tags) node.tags = {};
8510
+ const tags = node.tags;
8511
+ const value = assignments.get(node.id);
8512
+ if (value !== void 0) {
8513
+ tags[tagger.tagKey] = value;
8514
+ } else if (tagger.trackUntagged) {
8515
+ tags[tagger.tagKey] = "untagged";
8516
+ }
8517
+ }
8518
+ }
8519
+ for (const node of taggedNodes) {
8520
+ const manual = manualTags[node.id];
8521
+ if (manual) {
8522
+ if (!node.tags) node.tags = {};
8523
+ const tags = node.tags;
8524
+ Object.assign(tags, manual);
8525
+ }
8526
+ }
8527
+ return { ...graph, nodes: taggedNodes };
8528
+ }
8529
+ function readGraphRaw(rootDir, layer) {
8193
8530
  const filePath = graphFilePath(rootDir, layer);
8194
- if (!(0, import_node_fs9.existsSync)(filePath)) return null;
8195
- const stat = (0, import_node_fs9.statSync)(filePath);
8531
+ if (!(0, import_node_fs11.existsSync)(filePath)) return null;
8532
+ const stat = (0, import_node_fs11.statSync)(filePath);
8196
8533
  const cached = graphCache.get(filePath);
8197
8534
  if (cached && cached.mtimeMs === stat.mtimeMs) {
8198
8535
  return cached.graph;
8199
8536
  }
8200
- const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
8537
+ const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
8201
8538
  const graph = JSON.parse(content);
8202
8539
  graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
8203
8540
  return graph;
8204
8541
  }
8542
+ function readGraph(rootDir, layer) {
8543
+ const rawFilePath = graphFilePath(rootDir, layer);
8544
+ if (!(0, import_node_fs11.existsSync)(rawFilePath)) return null;
8545
+ const rawMtime = getMtimeMs(rawFilePath);
8546
+ const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
8547
+ const cacheKey = `${rootDir}:${layer}`;
8548
+ const cached = taggedCache.get(cacheKey);
8549
+ if (cached && cached.rawMtimeMs === rawMtime && cached.tagsMtimeMs === tagsMtime) {
8550
+ return cached.graph;
8551
+ }
8552
+ const raw = readGraphRaw(rootDir, layer);
8553
+ if (!raw) return null;
8554
+ const tagged = applyTags(raw, layer, rootDir);
8555
+ taggedCache.set(cacheKey, { rawMtimeMs: rawMtime, tagsMtimeMs: tagsMtime, graph: tagged });
8556
+ return tagged;
8557
+ }
8205
8558
  function readAllGraphs(rootDir) {
8206
8559
  const result = {};
8207
8560
  for (const layer of LAYERS) {
@@ -8212,12 +8565,13 @@ function readAllGraphs(rootDir) {
8212
8565
  }
8213
8566
  function generateGraph(rootDir, layer) {
8214
8567
  const dir = graphsDir(rootDir);
8215
- (0, import_node_fs9.mkdirSync)(dir, { recursive: true });
8568
+ (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
8216
8569
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
8217
8570
  for (const result of results) {
8218
8571
  const filePath = graphFilePath(rootDir, result.layer);
8219
- (0, import_node_fs9.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
8572
+ (0, import_node_fs11.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
8220
8573
  invalidateCache(filePath);
8574
+ invalidateTaggedCache(rootDir, result.layer);
8221
8575
  }
8222
8576
  return results;
8223
8577
  }
@@ -8278,27 +8632,46 @@ function handleGraphCommand(subcommand, args) {
8278
8632
  }
8279
8633
 
8280
8634
  // src/server/graph-mcp.ts
8281
- var import_node_fs11 = require("node:fs");
8282
- var import_node_path12 = require("node:path");
8635
+ var import_node_fs13 = require("node:fs");
8636
+ var import_node_path15 = require("node:path");
8283
8637
  var import_node_child_process2 = require("node:child_process");
8284
8638
  var import_node_os2 = require("node:os");
8285
8639
 
8286
8640
  // src/server/lockfile.ts
8287
8641
  var import_node_child_process = require("node:child_process");
8288
- var import_node_fs10 = require("node:fs");
8642
+ var import_node_fs12 = require("node:fs");
8289
8643
  var import_node_os = require("node:os");
8290
- var import_node_path11 = require("node:path");
8291
- function lockDir() {
8292
- return (0, import_node_path11.join)((0, import_node_os.homedir)(), ".launchsecure");
8293
- }
8294
- function lockPath() {
8295
- return (0, import_node_path11.join)(lockDir(), "launch-chart.lock");
8296
- }
8297
- function readLock() {
8298
- const p = lockPath();
8299
- if (!(0, import_node_fs10.existsSync)(p)) return null;
8644
+ var import_node_path14 = require("node:path");
8645
+ function lockDir(projectRoot) {
8646
+ if (projectRoot) {
8647
+ return (0, import_node_path14.join)(projectRoot, ".launchsecure");
8648
+ }
8649
+ return (0, import_node_path14.join)((0, import_node_os.homedir)(), ".launchsecure");
8650
+ }
8651
+ function lockPath(projectRoot) {
8652
+ return (0, import_node_path14.join)(lockDir(projectRoot), "launch-chart.lock");
8653
+ }
8654
+ var _activeProjectRoot;
8655
+ function readLock(projectRoot) {
8656
+ const root = projectRoot ?? _activeProjectRoot;
8657
+ const p = lockPath(root);
8658
+ if (!(0, import_node_fs12.existsSync)(p)) {
8659
+ if (root) {
8660
+ const globalP = lockPath();
8661
+ if ((0, import_node_fs12.existsSync)(globalP)) {
8662
+ try {
8663
+ const data = JSON.parse((0, import_node_fs12.readFileSync)(globalP, "utf-8"));
8664
+ if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
8665
+ return data;
8666
+ }
8667
+ } catch {
8668
+ }
8669
+ }
8670
+ }
8671
+ return null;
8672
+ }
8300
8673
  try {
8301
- const data = JSON.parse((0, import_node_fs10.readFileSync)(p, "utf-8"));
8674
+ const data = JSON.parse((0, import_node_fs12.readFileSync)(p, "utf-8"));
8302
8675
  if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
8303
8676
  return data;
8304
8677
  } catch {
@@ -8327,28 +8700,31 @@ function getListenerPid(port) {
8327
8700
  return null;
8328
8701
  }
8329
8702
  }
8330
- function getLiveLock() {
8331
- const lock = readLock();
8703
+ function getLiveLock(projectRoot) {
8704
+ const root = projectRoot ?? _activeProjectRoot;
8705
+ const lock = readLock(root);
8332
8706
  if (!lock) return null;
8333
8707
  const listenerPid = getListenerPid(lock.port);
8334
8708
  const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
8335
8709
  if (!live) {
8336
8710
  try {
8337
- (0, import_node_fs10.unlinkSync)(lockPath());
8711
+ (0, import_node_fs12.unlinkSync)(lockPath(root));
8338
8712
  } catch {
8339
8713
  }
8340
8714
  return null;
8341
8715
  }
8342
8716
  return lock;
8343
8717
  }
8344
- function clearLock() {
8718
+ function clearLock(projectRoot) {
8719
+ const root = projectRoot ?? _activeProjectRoot;
8345
8720
  try {
8346
- (0, import_node_fs10.unlinkSync)(lockPath());
8721
+ (0, import_node_fs12.unlinkSync)(lockPath(root));
8347
8722
  } catch {
8348
8723
  }
8349
8724
  }
8350
8725
 
8351
8726
  // src/server/graph-mcp.ts
8727
+ init_config();
8352
8728
  var SERVER_INFO = {
8353
8729
  name: "launchsecure-graph",
8354
8730
  version: "0.0.1"
@@ -8370,7 +8746,7 @@ var TOOLS = [
8370
8746
  },
8371
8747
  {
8372
8748
  name: "read_graph",
8373
- description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module (ui layer only: auth, admin, project, org, settings, integrations, shared-ui, layout, root)\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
8749
+ description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
8374
8750
  inputSchema: {
8375
8751
  type: "object",
8376
8752
  properties: {
@@ -8389,7 +8765,15 @@ var TOOLS = [
8389
8765
  },
8390
8766
  module: {
8391
8767
  type: "string",
8392
- description: 'UI layer only \u2014 filter by module (e.g. "auth", "admin", "project", "org").'
8768
+ description: 'Filter by module tag (e.g. "auth", "admin", "settings"). Works across all layers.'
8769
+ },
8770
+ tag_key: {
8771
+ type: "string",
8772
+ description: "Filter by arbitrary tag key. Must be used with tag_value."
8773
+ },
8774
+ tag_value: {
8775
+ type: "string",
8776
+ description: "Filter by tag value for the given tag_key."
8393
8777
  },
8394
8778
  node_id: {
8395
8779
  type: "string",
@@ -8513,6 +8897,46 @@ Use this when the user asks "is the chart running", "show me the project graph U
8513
8897
  type: "object",
8514
8898
  properties: {}
8515
8899
  }
8900
+ },
8901
+ {
8902
+ name: "add_tag",
8903
+ description: 'Tag a graph node with a key-value pair. Tags persist in .launchsecure/graphs/tags.json and survive graph regeneration. Use for annotating nodes with arbitrary metadata (e.g. "refactor_later", "owner", "priority"). Manual tags override computed tags (like module and screen) for the same key.',
8904
+ inputSchema: {
8905
+ type: "object",
8906
+ properties: {
8907
+ node_id: {
8908
+ type: "string",
8909
+ description: 'The node id to tag (e.g. "app/(auth)/login/page.tsx").'
8910
+ },
8911
+ key: {
8912
+ type: "string",
8913
+ description: 'Tag key (e.g. "module", "owner", "refactor_later").'
8914
+ },
8915
+ value: {
8916
+ type: "string",
8917
+ description: 'Tag value (e.g. "auth", "alice", "true").'
8918
+ }
8919
+ },
8920
+ required: ["node_id", "key", "value"]
8921
+ }
8922
+ },
8923
+ {
8924
+ name: "remove_tag",
8925
+ description: "Remove a manual tag from a graph node. Only removes tags from tags.json \u2014 computed tags (module, screen) cannot be removed (they are re-derived at read time).",
8926
+ inputSchema: {
8927
+ type: "object",
8928
+ properties: {
8929
+ node_id: {
8930
+ type: "string",
8931
+ description: "The node id to remove the tag from."
8932
+ },
8933
+ key: {
8934
+ type: "string",
8935
+ description: "Tag key to remove."
8936
+ }
8937
+ },
8938
+ required: ["node_id", "key"]
8939
+ }
8516
8940
  }
8517
8941
  ];
8518
8942
  function matchesSearch(node, query) {
@@ -8526,7 +8950,7 @@ function matchesSearch(node, query) {
8526
8950
  function toMinimal(nodes) {
8527
8951
  return nodes.map((n) => {
8528
8952
  const out = { id: n.id, type: n.type, name: n.name };
8529
- if (n.module != null) out.module = n.module;
8953
+ if (n.tags != null) out.tags = n.tags;
8530
8954
  if (n.route != null) out.route = n.route;
8531
8955
  if (n.methods != null) out.methods = n.methods;
8532
8956
  return out;
@@ -8537,11 +8961,12 @@ var COMPACT_SCHEMA = {
8537
8961
  i: "id",
8538
8962
  t: "type",
8539
8963
  n: "name",
8540
- m: "module",
8964
+ m: "module (from tags)",
8541
8965
  r: "route",
8542
8966
  mt: "methods",
8543
8967
  x: "exports",
8544
- c: "columns"
8968
+ c: "columns",
8969
+ tg: "tags"
8545
8970
  },
8546
8971
  edges: {
8547
8972
  s: "source_node_index",
@@ -8559,7 +8984,8 @@ var COMPACT_NODE_KNOWN_KEYS = /* @__PURE__ */ new Set([
8559
8984
  "route",
8560
8985
  "methods",
8561
8986
  "exports",
8562
- "columns"
8987
+ "columns",
8988
+ "tags"
8563
8989
  ]);
8564
8990
  var EST_CHARS_PER_NODE_FULL = {
8565
8991
  ui: 300,
@@ -8580,11 +9006,13 @@ var NEIGHBORHOOD_BUDGET_CHARS = 55e3;
8580
9006
  var BATCH_BUDGET_CHARS = 6e4;
8581
9007
  function toCompactNode(n) {
8582
9008
  const out = { i: n.id, t: n.type, n: n.name };
8583
- if (n.module != null) out.m = n.module;
9009
+ const tags = n.tags;
9010
+ if (tags?.module) out.m = tags.module;
8584
9011
  if (n.route != null) out.r = n.route;
8585
9012
  if (n.methods != null) out.mt = n.methods;
8586
9013
  if (n.exports != null) out.x = n.exports;
8587
9014
  if (n.columns != null) out.c = n.columns;
9015
+ if (tags != null) out.tg = tags;
8588
9016
  for (const k of Object.keys(n)) {
8589
9017
  if (!COMPACT_NODE_KNOWN_KEYS.has(k) && n[k] != null) out[k] = n[k];
8590
9018
  }
@@ -8660,7 +9088,8 @@ function layerSummary(graph) {
8660
9088
  const moduleCounts = {};
8661
9089
  for (const n of graph.nodes) {
8662
9090
  typeCounts[n.type] = (typeCounts[n.type] ?? 0) + 1;
8663
- const mod = n.module;
9091
+ const tags = n.tags;
9092
+ const mod = tags?.module;
8664
9093
  if (mod) moduleCounts[mod] = (moduleCounts[mod] ?? 0) + 1;
8665
9094
  }
8666
9095
  const edgeTypeCounts = {};
@@ -8717,12 +9146,14 @@ function runReadGraphQueryRaw(rootDir, args) {
8717
9146
  const search = args.search;
8718
9147
  const type = args.type;
8719
9148
  const module_ = args.module;
9149
+ const tagKey = args.tag_key;
9150
+ const tagValue = args.tag_value;
8720
9151
  const nodeId = args.node_id;
8721
9152
  const hops = args.hops ?? 1;
8722
9153
  const layerIsDb = args.layer === "db";
8723
9154
  const minimal = args.minimal ?? layerIsDb;
8724
9155
  const includeEdges = args.include_edges;
8725
- const hasFilter = !!(search || type || module_ || nodeId);
9156
+ const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
8726
9157
  if (layer && !["ui", "api", "db"].includes(layer)) {
8727
9158
  return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
8728
9159
  }
@@ -8778,7 +9209,9 @@ function runReadGraphQueryRaw(rootDir, args) {
8778
9209
  const matched = graph.nodes.filter((n) => {
8779
9210
  if (search && !matchesSearch(n, search)) return false;
8780
9211
  if (type && n.type !== type) return false;
8781
- if (module_ && n.module !== module_) return false;
9212
+ const nodeTags = n.tags;
9213
+ if (module_ && nodeTags?.module !== module_) return false;
9214
+ if (tagKey && tagValue && nodeTags?.[tagKey] !== tagValue) return false;
8782
9215
  return true;
8783
9216
  });
8784
9217
  const matchedIds = new Set(matched.map((n) => n.id));
@@ -8865,9 +9298,9 @@ function handleReadGraph(args) {
8865
9298
  return okJson(result);
8866
9299
  }
8867
9300
  function nodeToFilePath(rootDir, layer, nodeId) {
8868
- if (layer === "ui") return (0, import_node_path12.join)(rootDir, "src", nodeId);
8869
- if (layer === "api") return (0, import_node_path12.join)(rootDir, nodeId);
8870
- if (layer === "db") return (0, import_node_path12.join)(rootDir, "prisma", "schema.prisma");
9301
+ if (layer === "ui") return (0, import_node_path15.join)(rootDir, "src", nodeId);
9302
+ if (layer === "api") return (0, import_node_path15.join)(rootDir, nodeId);
9303
+ if (layer === "db") return (0, import_node_path15.join)(rootDir, "prisma", "schema.prisma");
8871
9304
  return null;
8872
9305
  }
8873
9306
  function handleGrepNodes(args) {
@@ -8927,11 +9360,11 @@ function handleGrepNodes(args) {
8927
9360
  let filesSearched = 0;
8928
9361
  let truncated = false;
8929
9362
  for (const [filePath, nodeId] of filePaths) {
8930
- if (!(0, import_node_fs11.existsSync)(filePath)) continue;
9363
+ if (!(0, import_node_fs13.existsSync)(filePath)) continue;
8931
9364
  filesSearched++;
8932
9365
  let content;
8933
9366
  try {
8934
- content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
9367
+ content = (0, import_node_fs13.readFileSync)(filePath, "utf-8");
8935
9368
  } catch {
8936
9369
  continue;
8937
9370
  }
@@ -8969,7 +9402,8 @@ function handleGrepNodes(args) {
8969
9402
  });
8970
9403
  }
8971
9404
  function handleChartServerStatus() {
8972
- const lock = getLiveLock();
9405
+ const rootDir = process.cwd();
9406
+ const lock = getLiveLock(rootDir);
8973
9407
  if (!lock) {
8974
9408
  return okJson({ running: false });
8975
9409
  }
@@ -8983,7 +9417,8 @@ function handleChartServerStatus() {
8983
9417
  });
8984
9418
  }
8985
9419
  function handleStartChartServer(args) {
8986
- const lock = getLiveLock();
9420
+ const rootDir = process.cwd();
9421
+ const lock = getLiveLock(rootDir);
8987
9422
  if (lock) {
8988
9423
  return okJson({
8989
9424
  started: false,
@@ -8994,11 +9429,11 @@ function handleStartChartServer(args) {
8994
9429
  });
8995
9430
  }
8996
9431
  const entryPath = process.argv[1];
8997
- const logDir = (0, import_node_path12.join)((0, import_node_os2.homedir)(), ".launchsecure");
8998
- (0, import_node_fs11.mkdirSync)(logDir, { recursive: true });
8999
- const logPath = (0, import_node_path12.join)(logDir, "launch-chart.log");
9000
- const out = (0, import_node_fs11.openSync)(logPath, "a");
9001
- const err2 = (0, import_node_fs11.openSync)(logPath, "a");
9432
+ const logDir = (0, import_node_path15.join)((0, import_node_os2.homedir)(), ".launchsecure");
9433
+ (0, import_node_fs13.mkdirSync)(logDir, { recursive: true });
9434
+ const logPath = (0, import_node_path15.join)(logDir, "launch-chart.log");
9435
+ const out = (0, import_node_fs13.openSync)(logPath, "a");
9436
+ const err2 = (0, import_node_fs13.openSync)(logPath, "a");
9002
9437
  const portArgs = args.port ? ["--port", String(args.port)] : [];
9003
9438
  const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
9004
9439
  detached: true,
@@ -9013,7 +9448,8 @@ function handleStartChartServer(args) {
9013
9448
  });
9014
9449
  }
9015
9450
  function handleStopChartServer() {
9016
- const lock = getLiveLock();
9451
+ const rootDir = process.cwd();
9452
+ const lock = getLiveLock(rootDir);
9017
9453
  if (!lock) {
9018
9454
  return okJson({ stopped: false, reason: "not_running" });
9019
9455
  }
@@ -9023,14 +9459,45 @@ function handleStopChartServer() {
9023
9459
  } catch (e) {
9024
9460
  const code = e.code;
9025
9461
  if (code === "ESRCH") {
9026
- clearLock();
9462
+ clearLock(rootDir);
9027
9463
  return okJson({ stopped: true, pid: lock.pid, note: "process was already gone, lock cleaned up" });
9028
9464
  }
9029
9465
  return okJson({ stopped: false, reason: `kill failed: ${code ?? e}` });
9030
9466
  }
9031
9467
  }
9468
+ function handleAddTag(args) {
9469
+ const rootDir = process.cwd();
9470
+ const nodeId = args.node_id;
9471
+ const key = args.key;
9472
+ const value = args.value;
9473
+ if (!nodeId) return err("node_id is required");
9474
+ if (!key) return err("key is required");
9475
+ if (!value) return err("value is required");
9476
+ const graphs = readAllGraphs(rootDir);
9477
+ let found = false;
9478
+ for (const graph of Object.values(graphs)) {
9479
+ if (graph && graph.nodes.some((n) => n.id === nodeId)) {
9480
+ found = true;
9481
+ break;
9482
+ }
9483
+ }
9484
+ if (!found) {
9485
+ return err(`Node "${nodeId}" not found in any graph layer. Check the node_id.`);
9486
+ }
9487
+ setTag(rootDir, nodeId, key, value);
9488
+ return okJson({ ok: true, node_id: nodeId, tag: { [key]: value } });
9489
+ }
9490
+ function handleRemoveTag(args) {
9491
+ const rootDir = process.cwd();
9492
+ const nodeId = args.node_id;
9493
+ const key = args.key;
9494
+ if (!nodeId) return err("node_id is required");
9495
+ if (!key) return err("key is required");
9496
+ removeTag(rootDir, nodeId, key);
9497
+ return okJson({ ok: true, node_id: nodeId, removed_key: key });
9498
+ }
9032
9499
  function handleDetectProjectStack() {
9033
- const rootDir = findProjectRoot(process.cwd());
9500
+ const rootDir = process.cwd();
9034
9501
  const parsers = [
9035
9502
  { id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
9036
9503
  { id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
@@ -9048,20 +9515,20 @@ function handleDetectProjectStack() {
9048
9515
  if (f.type === "out_of_pattern") stats.out_of_pattern++;
9049
9516
  }
9050
9517
  }
9051
- const srcDir = (0, import_node_path12.join)(rootDir, "src");
9052
- if ((0, import_node_fs11.existsSync)(srcDir)) {
9518
+ const srcDir = (0, import_node_path15.join)(rootDir, "src");
9519
+ if ((0, import_node_fs13.existsSync)(srcDir)) {
9053
9520
  const scanDir = (dir) => {
9054
- if (!(0, import_node_fs11.existsSync)(dir)) return;
9055
- for (const entry of (0, import_node_fs11.readdirSync)(dir, { withFileTypes: true })) {
9521
+ if (!(0, import_node_fs13.existsSync)(dir)) return;
9522
+ for (const entry of (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true })) {
9056
9523
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
9057
- const full = (0, import_node_path12.join)(dir, entry.name);
9524
+ const full = (0, import_node_path15.join)(dir, entry.name);
9058
9525
  if (entry.isDirectory()) {
9059
9526
  scanDir(full);
9060
9527
  continue;
9061
9528
  }
9062
- if (![".ts", ".tsx"].includes((0, import_node_path12.extname)(entry.name))) continue;
9529
+ if (![".ts", ".tsx"].includes((0, import_node_path15.extname)(entry.name))) continue;
9063
9530
  try {
9064
- const content = (0, import_node_fs11.readFileSync)(full, "utf-8");
9531
+ const content = (0, import_node_fs13.readFileSync)(full, "utf-8");
9065
9532
  const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
9066
9533
  if (matches) stats.annotations += matches.length;
9067
9534
  } catch {
@@ -9148,6 +9615,14 @@ function handleMessage(msg) {
9148
9615
  respond(id ?? null, handleDetectProjectStack());
9149
9616
  return;
9150
9617
  }
9618
+ if (toolName === "add_tag") {
9619
+ respond(id ?? null, handleAddTag(args));
9620
+ return;
9621
+ }
9622
+ if (toolName === "remove_tag") {
9623
+ respond(id ?? null, handleRemoveTag(args));
9624
+ return;
9625
+ }
9151
9626
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
9152
9627
  return;
9153
9628
  }
@@ -9245,7 +9720,7 @@ function parseArgs() {
9245
9720
  return { port, token, serverUrl: LAUNCHSECURE_URL, subcommand };
9246
9721
  }
9247
9722
  function tryListen(server, port, maxRetries = 10) {
9248
- return new Promise((resolve2, reject) => {
9723
+ return new Promise((resolve3, reject) => {
9249
9724
  let attempts = 0;
9250
9725
  function attempt(p) {
9251
9726
  server.once("error", (err2) => {
@@ -9256,7 +9731,7 @@ function tryListen(server, port, maxRetries = 10) {
9256
9731
  reject(err2);
9257
9732
  }
9258
9733
  });
9259
- server.listen(p, () => resolve2(p));
9734
+ server.listen(p, () => resolve3(p));
9260
9735
  }
9261
9736
  attempt(port);
9262
9737
  });
@@ -9277,7 +9752,7 @@ function saveCredentials(creds) {
9277
9752
  });
9278
9753
  }
9279
9754
  function verifyToken(serverUrl, token) {
9280
- return new Promise((resolve2) => {
9755
+ return new Promise((resolve3) => {
9281
9756
  const url = new URL("/api/mcp/verify", serverUrl);
9282
9757
  const body = JSON.stringify({ token });
9283
9758
  const mod = url.protocol === "https:" ? import_https.default : import_http.default;
@@ -9292,30 +9767,30 @@ function verifyToken(serverUrl, token) {
9292
9767
  res.on("data", (chunk) => data += chunk);
9293
9768
  res.on("end", () => {
9294
9769
  try {
9295
- resolve2(JSON.parse(data));
9770
+ resolve3(JSON.parse(data));
9296
9771
  } catch {
9297
- resolve2({ valid: false, error: "Invalid response from server" });
9772
+ resolve3({ valid: false, error: "Invalid response from server" });
9298
9773
  }
9299
9774
  });
9300
9775
  });
9301
9776
  req.on("error", (err2) => {
9302
- resolve2({ valid: false, error: `Cannot reach server: ${err2.message}` });
9777
+ resolve3({ valid: false, error: `Cannot reach server: ${err2.message}` });
9303
9778
  });
9304
9779
  req.setTimeout(1e4, () => {
9305
9780
  req.destroy();
9306
- resolve2({ valid: false, error: "Connection timed out" });
9781
+ resolve3({ valid: false, error: "Connection timed out" });
9307
9782
  });
9308
9783
  req.write(body);
9309
9784
  req.end();
9310
9785
  });
9311
9786
  }
9312
9787
  function httpRequest(reqUrl, options, body, timeout = 3e4) {
9313
- return new Promise((resolve2, reject) => {
9788
+ return new Promise((resolve3, reject) => {
9314
9789
  const mod = reqUrl.protocol === "https:" ? import_https.default : import_http.default;
9315
9790
  const r = mod.request(reqUrl, options, (resp) => {
9316
9791
  let data = "";
9317
9792
  resp.on("data", (chunk) => data += chunk);
9318
- resp.on("end", () => resolve2({ status: resp.statusCode || 0, headers: resp.headers, body: data }));
9793
+ resp.on("end", () => resolve3({ status: resp.statusCode || 0, headers: resp.headers, body: data }));
9319
9794
  });
9320
9795
  r.on("error", reject);
9321
9796
  r.setTimeout(timeout, () => {