archtracker-mcp 0.5.0 → 0.6.0

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.
package/dist/mcp/index.js CHANGED
@@ -2226,44 +2226,139 @@ function mergeLayerGraphs(projectRoot, layers) {
2226
2226
  };
2227
2227
  }
2228
2228
 
2229
- // src/storage/snapshot.ts
2230
- import { mkdir, writeFile, readFile as readFile2, access } from "fs/promises";
2229
+ // src/storage/layers.ts
2230
+ import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
2231
2231
  import { join as join5 } from "path";
2232
2232
  import { z } from "zod";
2233
+ var ARCHTRACKER_DIR = ".archtracker";
2234
+ var LAYERS_FILE = "layers.json";
2235
+ var LayerDefinitionSchema = z.object({
2236
+ name: z.string().min(1).regex(
2237
+ /^[a-zA-Z0-9_-]+$/,
2238
+ "Layer name must be alphanumeric (hyphens/underscores allowed)"
2239
+ ),
2240
+ targetDir: z.string().min(1),
2241
+ language: z.enum(LANGUAGE_IDS).optional(),
2242
+ exclude: z.array(z.string()).optional(),
2243
+ color: z.string().optional(),
2244
+ description: z.string().optional()
2245
+ });
2246
+ var CrossLayerConnectionSchema = z.object({
2247
+ fromLayer: z.string(),
2248
+ fromFile: z.string(),
2249
+ toLayer: z.string(),
2250
+ toFile: z.string(),
2251
+ type: z.enum(["api-call", "event", "data-flow", "manual"]),
2252
+ label: z.string().optional()
2253
+ });
2254
+ var LayerConfigSchema = z.object({
2255
+ version: z.literal("1.0"),
2256
+ layers: z.array(LayerDefinitionSchema).min(1).refine(
2257
+ (layers) => {
2258
+ const names = layers.map((l) => l.name);
2259
+ return new Set(names).size === names.length;
2260
+ },
2261
+ { message: "Layer names must be unique" }
2262
+ ),
2263
+ connections: z.array(CrossLayerConnectionSchema).optional()
2264
+ });
2265
+ async function loadLayerConfig(projectRoot) {
2266
+ const filePath = join5(projectRoot, ARCHTRACKER_DIR, LAYERS_FILE);
2267
+ let raw;
2268
+ try {
2269
+ raw = await readFile2(filePath, "utf-8");
2270
+ } catch (error) {
2271
+ if (isNodeError(error) && error.code === "ENOENT") {
2272
+ return null;
2273
+ }
2274
+ throw new Error(`Failed to read ${filePath}`);
2275
+ }
2276
+ let parsed;
2277
+ try {
2278
+ parsed = JSON.parse(raw);
2279
+ } catch {
2280
+ throw new Error(`Invalid JSON in ${filePath}`);
2281
+ }
2282
+ const result = LayerConfigSchema.safeParse(parsed);
2283
+ if (!result.success) {
2284
+ const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).slice(0, 5).join("\n");
2285
+ throw new Error(`layers.json validation failed:
2286
+ ${issues}`);
2287
+ }
2288
+ return result.data;
2289
+ }
2290
+ function isNodeError(error) {
2291
+ return error instanceof Error && "code" in error;
2292
+ }
2293
+
2294
+ // src/analyzer/resolve.ts
2295
+ async function resolveGraph(opts) {
2296
+ const layerConfig = await loadLayerConfig(opts.projectRoot);
2297
+ if (layerConfig) {
2298
+ const multi = await analyzeMultiLayer(opts.projectRoot, layerConfig.layers);
2299
+ const autoConnections = detectCrossLayerConnections(multi.layers, layerConfig.layers);
2300
+ const manualConnections = layerConfig.connections ?? [];
2301
+ const manualKeys = new Set(manualConnections.map(
2302
+ (c) => `${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`
2303
+ ));
2304
+ const merged = [
2305
+ ...manualConnections,
2306
+ ...autoConnections.filter(
2307
+ (c) => !manualKeys.has(`${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`)
2308
+ )
2309
+ ];
2310
+ return {
2311
+ graph: multi.merged,
2312
+ multiLayer: multi,
2313
+ layerMetadata: multi.layerMetadata,
2314
+ crossLayerEdges: merged
2315
+ };
2316
+ }
2317
+ const graph = await analyzeProject(opts.targetDir, {
2318
+ exclude: opts.exclude,
2319
+ language: opts.language
2320
+ });
2321
+ return { graph };
2322
+ }
2323
+
2324
+ // src/storage/snapshot.ts
2325
+ import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile3, access } from "fs/promises";
2326
+ import { join as join6 } from "path";
2327
+ import { z as z2 } from "zod";
2233
2328
 
2234
2329
  // src/types/schema.ts
2235
2330
  var SCHEMA_VERSION = "1.1";
2236
2331
 
2237
2332
  // src/storage/snapshot.ts
2238
- var ARCHTRACKER_DIR = ".archtracker";
2333
+ var ARCHTRACKER_DIR2 = ".archtracker";
2239
2334
  var SNAPSHOT_FILE = "snapshot.json";
2240
- var FileNodeSchema = z.object({
2241
- path: z.string(),
2242
- exists: z.boolean(),
2243
- dependencies: z.array(z.string()),
2244
- dependents: z.array(z.string())
2335
+ var FileNodeSchema = z2.object({
2336
+ path: z2.string(),
2337
+ exists: z2.boolean(),
2338
+ dependencies: z2.array(z2.string()),
2339
+ dependents: z2.array(z2.string())
2245
2340
  });
2246
- var DependencyGraphSchema = z.object({
2247
- rootDir: z.string(),
2248
- files: z.record(z.string(), FileNodeSchema),
2249
- edges: z.array(z.object({
2250
- source: z.string(),
2251
- target: z.string(),
2252
- type: z.enum(["static", "dynamic", "type-only"])
2341
+ var DependencyGraphSchema = z2.object({
2342
+ rootDir: z2.string(),
2343
+ files: z2.record(z2.string(), FileNodeSchema),
2344
+ edges: z2.array(z2.object({
2345
+ source: z2.string(),
2346
+ target: z2.string(),
2347
+ type: z2.enum(["static", "dynamic", "type-only"])
2253
2348
  })),
2254
- circularDependencies: z.array(z.object({ cycle: z.array(z.string()) })),
2255
- totalFiles: z.number(),
2256
- totalEdges: z.number()
2349
+ circularDependencies: z2.array(z2.object({ cycle: z2.array(z2.string()) })),
2350
+ totalFiles: z2.number(),
2351
+ totalEdges: z2.number()
2257
2352
  });
2258
- var SnapshotSchema = z.object({
2259
- version: z.enum([SCHEMA_VERSION, "1.0"]),
2260
- timestamp: z.string(),
2261
- rootDir: z.string(),
2353
+ var SnapshotSchema = z2.object({
2354
+ version: z2.enum([SCHEMA_VERSION, "1.0"]),
2355
+ timestamp: z2.string(),
2356
+ rootDir: z2.string(),
2262
2357
  graph: DependencyGraphSchema
2263
2358
  });
2264
2359
  async function saveSnapshot(projectRoot, graph, multiLayer) {
2265
- const dirPath = join5(projectRoot, ARCHTRACKER_DIR);
2266
- const filePath = join5(dirPath, SNAPSHOT_FILE);
2360
+ const dirPath = join6(projectRoot, ARCHTRACKER_DIR2);
2361
+ const filePath = join6(dirPath, SNAPSHOT_FILE);
2267
2362
  const snapshot = {
2268
2363
  version: SCHEMA_VERSION,
2269
2364
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2271,17 +2366,17 @@ async function saveSnapshot(projectRoot, graph, multiLayer) {
2271
2366
  graph,
2272
2367
  ...multiLayer ? { multiLayer } : {}
2273
2368
  };
2274
- await mkdir(dirPath, { recursive: true });
2275
- await writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf-8");
2369
+ await mkdir2(dirPath, { recursive: true });
2370
+ await writeFile2(filePath, JSON.stringify(snapshot, null, 2), "utf-8");
2276
2371
  return snapshot;
2277
2372
  }
2278
2373
  async function loadSnapshot(projectRoot) {
2279
- const filePath = join5(projectRoot, ARCHTRACKER_DIR, SNAPSHOT_FILE);
2374
+ const filePath = join6(projectRoot, ARCHTRACKER_DIR2, SNAPSHOT_FILE);
2280
2375
  let raw;
2281
2376
  try {
2282
- raw = await readFile2(filePath, "utf-8");
2377
+ raw = await readFile3(filePath, "utf-8");
2283
2378
  } catch (error) {
2284
- if (isNodeError(error) && error.code === "ENOENT") {
2379
+ if (isNodeError2(error) && error.code === "ENOENT") {
2285
2380
  return null;
2286
2381
  }
2287
2382
  throw new StorageError(
@@ -2312,7 +2407,7 @@ var StorageError = class extends Error {
2312
2407
  this.name = "StorageError";
2313
2408
  }
2314
2409
  };
2315
- function isNodeError(error) {
2410
+ function isNodeError2(error) {
2316
2411
  return error instanceof Error && "code" in error;
2317
2412
  }
2318
2413
 
@@ -2413,71 +2508,6 @@ function arraysEqual(a, b) {
2413
2508
  return true;
2414
2509
  }
2415
2510
 
2416
- // src/storage/layers.ts
2417
- import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
2418
- import { join as join6 } from "path";
2419
- import { z as z2 } from "zod";
2420
- var ARCHTRACKER_DIR2 = ".archtracker";
2421
- var LAYERS_FILE = "layers.json";
2422
- var LayerDefinitionSchema = z2.object({
2423
- name: z2.string().min(1).regex(
2424
- /^[a-zA-Z0-9_-]+$/,
2425
- "Layer name must be alphanumeric (hyphens/underscores allowed)"
2426
- ),
2427
- targetDir: z2.string().min(1),
2428
- language: z2.enum(LANGUAGE_IDS).optional(),
2429
- exclude: z2.array(z2.string()).optional(),
2430
- color: z2.string().optional(),
2431
- description: z2.string().optional()
2432
- });
2433
- var CrossLayerConnectionSchema = z2.object({
2434
- fromLayer: z2.string(),
2435
- fromFile: z2.string(),
2436
- toLayer: z2.string(),
2437
- toFile: z2.string(),
2438
- type: z2.enum(["api-call", "event", "data-flow", "manual"]),
2439
- label: z2.string().optional()
2440
- });
2441
- var LayerConfigSchema = z2.object({
2442
- version: z2.literal("1.0"),
2443
- layers: z2.array(LayerDefinitionSchema).min(1).refine(
2444
- (layers) => {
2445
- const names = layers.map((l) => l.name);
2446
- return new Set(names).size === names.length;
2447
- },
2448
- { message: "Layer names must be unique" }
2449
- ),
2450
- connections: z2.array(CrossLayerConnectionSchema).optional()
2451
- });
2452
- async function loadLayerConfig(projectRoot) {
2453
- const filePath = join6(projectRoot, ARCHTRACKER_DIR2, LAYERS_FILE);
2454
- let raw;
2455
- try {
2456
- raw = await readFile3(filePath, "utf-8");
2457
- } catch (error) {
2458
- if (isNodeError2(error) && error.code === "ENOENT") {
2459
- return null;
2460
- }
2461
- throw new Error(`Failed to read ${filePath}`);
2462
- }
2463
- let parsed;
2464
- try {
2465
- parsed = JSON.parse(raw);
2466
- } catch {
2467
- throw new Error(`Invalid JSON in ${filePath}`);
2468
- }
2469
- const result = LayerConfigSchema.safeParse(parsed);
2470
- if (!result.success) {
2471
- const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).slice(0, 5).join("\n");
2472
- throw new Error(`layers.json validation failed:
2473
- ${issues}`);
2474
- }
2475
- return result.data;
2476
- }
2477
- function isNodeError2(error) {
2478
- return error instanceof Error && "code" in error;
2479
- }
2480
-
2481
2511
  // src/utils/path-guard.ts
2482
2512
  import { resolve as resolve5 } from "path";
2483
2513
  function validatePath(inputPath, boundary) {
@@ -2527,30 +2557,13 @@ var LANG_DISPLAY = {
2527
2557
  "c-sharp": "C#"
2528
2558
  };
2529
2559
  var languageList = LANGUAGE_IDS.map((id) => LANG_DISPLAY[id] ?? id.charAt(0).toUpperCase() + id.slice(1)).join(", ");
2530
- async function resolveGraphForMcp(opts) {
2531
- if (opts.targetDir === "src") {
2532
- const layerConfig = await loadLayerConfig(opts.projectRoot);
2533
- if (layerConfig) {
2534
- const multi = await analyzeMultiLayer(opts.projectRoot, layerConfig.layers);
2535
- const autoConnections = detectCrossLayerConnections(multi.layers, layerConfig.layers);
2536
- const manualConnections = layerConfig.connections ?? [];
2537
- const manualKeys = new Set(manualConnections.map(
2538
- (c) => `${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`
2539
- ));
2540
- const allConnections = [
2541
- ...manualConnections,
2542
- ...autoConnections.filter(
2543
- (c) => !manualKeys.has(`${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`)
2544
- )
2545
- ];
2546
- return { graph: multi.merged, multiLayer: multi, layerMetadata: multi.layerMetadata, crossEdges: allConnections };
2547
- }
2548
- }
2549
- const graph = await analyzeProject(opts.targetDir, {
2560
+ async function resolveGraphMcp(opts) {
2561
+ return resolveGraph({
2562
+ targetDir: opts.targetDir,
2563
+ projectRoot: opts.projectRoot,
2550
2564
  exclude: opts.exclude,
2551
2565
  language: opts.language
2552
2566
  });
2553
- return { graph };
2554
2567
  }
2555
2568
  function formatLayerSummary(metadata) {
2556
2569
  return metadata.map(
@@ -2564,14 +2577,13 @@ server.tool(
2564
2577
  targetDir: z3.string().default("src").describe("Target directory path (default: src). When layers.json exists and this is 'src', multi-layer analysis is used automatically."),
2565
2578
  projectRoot: z3.string().default(".").describe("Project root (where .archtracker/ is located)"),
2566
2579
  exclude: z3.array(z3.string()).optional().describe("Array of regex patterns to exclude (e.g. ['test', 'mock'])"),
2567
- maxDepth: z3.number().int().min(0).optional().describe("Max analysis depth (0 = unlimited)"),
2568
2580
  language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
2569
2581
  },
2570
- async ({ targetDir, projectRoot, exclude, maxDepth, language }) => {
2582
+ async ({ targetDir, projectRoot, exclude, language }) => {
2571
2583
  try {
2572
2584
  validatePath(targetDir);
2573
2585
  validatePath(projectRoot);
2574
- const { graph, layerMetadata, crossEdges } = await resolveGraphForMcp({
2586
+ const { graph, layerMetadata, crossLayerEdges } = await resolveGraphMcp({
2575
2587
  targetDir,
2576
2588
  projectRoot,
2577
2589
  exclude,
@@ -2581,11 +2593,11 @@ server.tool(
2581
2593
  t("mcp.analyzeComplete", { files: graph.totalFiles, edges: graph.totalEdges }),
2582
2594
  graph.circularDependencies.length > 0 ? t("mcp.circularFound", { count: graph.circularDependencies.length }) : t("mcp.circularNone"),
2583
2595
  ...layerMetadata ? ["\nLayers:\n" + formatLayerSummary(layerMetadata)] : [],
2584
- ...crossEdges?.length ? [`
2585
- Cross-layer connections: ${crossEdges.length}`] : []
2596
+ ...crossLayerEdges?.length ? [`
2597
+ Cross-layer connections: ${crossLayerEdges.length}`] : []
2586
2598
  ].join("\n");
2587
2599
  const result = { ...graph };
2588
- if (crossEdges?.length) result.crossLayerConnections = crossEdges;
2600
+ if (crossLayerEdges?.length) result.crossLayerConnections = crossLayerEdges;
2589
2601
  return {
2590
2602
  content: [
2591
2603
  { type: "text", text: summary },
@@ -2599,19 +2611,20 @@ Cross-layer connections: ${crossEdges.length}`] : []
2599
2611
  );
2600
2612
  server.tool(
2601
2613
  "analyze_existing_architecture",
2602
- `Comprehensive architecture analysis for existing projects. Shows critical components, circular dependencies, orphan files, coupling hotspots, and directory breakdown. Supports ${LANGUAGE_IDS.length} languages.`,
2614
+ `Comprehensive architecture analysis for existing projects. Shows critical components, circular dependencies, orphan files, coupling hotspots, and directory breakdown. Supports ${languageList}.`,
2603
2615
  {
2604
2616
  targetDir: z3.string().default("src").describe("Target directory path (default: src)"),
2605
2617
  exclude: z3.array(z3.string()).optional().describe("Array of regex patterns to exclude"),
2606
2618
  topN: z3.number().int().min(1).max(50).optional().describe("Number of top items to show in each section (default: 10)"),
2607
2619
  saveSnapshot: z3.boolean().optional().describe("Also save a snapshot after analysis (default: false)"),
2608
- projectRoot: z3.string().default(".").describe("Project root (needed only when saveSnapshot is true)"),
2620
+ projectRoot: z3.string().default(".").describe("Project root (where .archtracker/ is located)"),
2609
2621
  language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
2610
2622
  },
2611
2623
  async ({ targetDir, exclude, topN, saveSnapshot: doSave, projectRoot, language }) => {
2612
2624
  try {
2613
2625
  validatePath(targetDir);
2614
- const { graph, multiLayer, layerMetadata, crossEdges } = await resolveGraphForMcp({
2626
+ validatePath(projectRoot);
2627
+ const { graph, multiLayer, layerMetadata, crossLayerEdges } = await resolveGraphMcp({
2615
2628
  targetDir,
2616
2629
  projectRoot,
2617
2630
  exclude,
@@ -2624,16 +2637,15 @@ server.tool(
2624
2637
  if (layerMetadata) {
2625
2638
  content.push({ type: "text", text: "\nLayers:\n" + formatLayerSummary(layerMetadata) });
2626
2639
  }
2627
- if (crossEdges?.length) {
2628
- const crossSummary = crossEdges.map(
2640
+ if (crossLayerEdges?.length) {
2641
+ const crossSummary = crossLayerEdges.map(
2629
2642
  (c) => ` ${c.fromLayer}/${c.fromFile} \u2192 ${c.toLayer}/${c.toFile} [${c.type}] ${c.label ?? ""}`
2630
2643
  ).join("\n");
2631
2644
  content.push({ type: "text", text: `
2632
- Cross-layer connections (${crossEdges.length}):
2645
+ Cross-layer connections (${crossLayerEdges.length}):
2633
2646
  ${crossSummary}` });
2634
2647
  }
2635
2648
  if (doSave) {
2636
- validatePath(projectRoot);
2637
2649
  await saveSnapshot(projectRoot, graph, multiLayer);
2638
2650
  content.push({ type: "text", text: t("analyze.snapshotSaved") });
2639
2651
  }
@@ -2655,7 +2667,7 @@ server.tool(
2655
2667
  try {
2656
2668
  validatePath(targetDir);
2657
2669
  validatePath(projectRoot);
2658
- const { graph, multiLayer, layerMetadata } = await resolveGraphForMcp({
2670
+ const { graph, multiLayer, layerMetadata } = await resolveGraphMcp({
2659
2671
  targetDir,
2660
2672
  projectRoot,
2661
2673
  language
@@ -2692,7 +2704,7 @@ server.tool(
2692
2704
  validatePath(projectRoot);
2693
2705
  const existingSnapshot = await loadSnapshot(projectRoot);
2694
2706
  if (!existingSnapshot) {
2695
- const { graph, multiLayer } = await resolveGraphForMcp({
2707
+ const { graph, multiLayer } = await resolveGraphMcp({
2696
2708
  targetDir,
2697
2709
  projectRoot,
2698
2710
  language
@@ -2711,7 +2723,7 @@ server.tool(
2711
2723
  ]
2712
2724
  };
2713
2725
  }
2714
- const { graph: currentGraph } = await resolveGraphForMcp({
2726
+ const { graph: currentGraph } = await resolveGraphMcp({
2715
2727
  targetDir,
2716
2728
  projectRoot,
2717
2729
  language
@@ -2734,9 +2746,11 @@ server.tool(
2734
2746
  },
2735
2747
  async ({ targetDir, projectRoot, language }) => {
2736
2748
  try {
2749
+ validatePath(targetDir);
2750
+ validatePath(projectRoot);
2737
2751
  let snapshot = await loadSnapshot(projectRoot);
2738
2752
  if (!snapshot) {
2739
- const { graph: graph2, multiLayer } = await resolveGraphForMcp({
2753
+ const { graph: graph2, multiLayer } = await resolveGraphMcp({
2740
2754
  targetDir,
2741
2755
  projectRoot,
2742
2756
  language
@@ -2802,7 +2816,7 @@ server.tool(
2802
2816
  validatePath(projectRoot);
2803
2817
  let snapshot = await loadSnapshot(projectRoot);
2804
2818
  if (!snapshot) {
2805
- const { graph: graph2, multiLayer } = await resolveGraphForMcp({
2819
+ const { graph: graph2, multiLayer } = await resolveGraphMcp({
2806
2820
  targetDir,
2807
2821
  projectRoot,
2808
2822
  language