archbyte 0.7.1 → 0.7.3

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/cli/stats.js CHANGED
@@ -2,10 +2,12 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import chalk from "chalk";
4
4
  import { resolveArchitecturePath, loadArchitectureFile, loadRulesConfig, getThreshold } from "./shared.js";
5
+ import { isJsonMode, outputSuccess } from "./output.js";
5
6
  /**
6
- * Print an architecture health dashboard to the terminal.
7
+ * Compute architecture stats as a structured object.
8
+ * Pure data function — no console output.
7
9
  */
8
- export async function handleStats(options) {
10
+ export function computeStats(options) {
9
11
  const diagramPath = resolveArchitecturePath(options);
10
12
  const arch = loadArchitectureFile(diagramPath);
11
13
  const config = loadRulesConfig(options.config);
@@ -14,12 +16,7 @@ export async function handleStats(options) {
14
16
  const databases = realNodes.filter((n) => n.type === "database");
15
17
  const externals = realNodes.filter((n) => n.type === "external");
16
18
  const totalConnections = arch.edges.length;
17
- // Auto-detect project name from cwd
18
- const projectName = process.cwd().split("/").pop() || "project";
19
- console.log();
20
- console.log(chalk.bold.cyan(`⚡ ArchByte Stats: ${projectName}`));
21
- console.log();
22
- // ── Scan Info (from metadata.json, fallback to analysis.json) ──
19
+ // Scan metadata
23
20
  const metadataJsonPath = path.join(process.cwd(), ".archbyte", "metadata.json");
24
21
  const analysisPath = path.join(process.cwd(), ".archbyte", "analysis.json");
25
22
  let scanMeta = {};
@@ -33,113 +30,80 @@ export async function handleStats(options) {
33
30
  }
34
31
  }
35
32
  catch { /* ignore */ }
36
- if (scanMeta.analyzedAt || scanMeta.durationMs || scanMeta.filesScanned) {
37
- const parts = [];
38
- if (scanMeta.durationMs != null) {
39
- parts.push(`${(scanMeta.durationMs / 1000).toFixed(1)}s`);
40
- }
41
- if (scanMeta.filesScanned != null) {
42
- parts.push(`${scanMeta.filesScanned} files scanned`);
43
- }
44
- if (scanMeta.tokenUsage) {
45
- const total = scanMeta.tokenUsage.input + scanMeta.tokenUsage.output;
46
- parts.push(`${total.toLocaleString()} tokens`);
47
- }
48
- if (scanMeta.mode) {
49
- parts.push(scanMeta.mode === "pipeline" ? "static + model" : "static only");
50
- }
51
- if (scanMeta.analyzedAt) {
52
- const d = new Date(scanMeta.analyzedAt);
53
- parts.push(d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" }));
54
- }
55
- console.log(chalk.gray(` Last scan: ${parts.join(" | ")}`));
56
- console.log();
57
- }
58
- // ── Summary ──
59
- console.log(` Components: ${chalk.bold(String(components.length))} Connections: ${chalk.bold(String(totalConnections))}`);
60
- console.log(` Databases: ${chalk.bold(String(databases.length))} Ext Services: ${chalk.bold(String(externals.length))}`);
61
- console.log();
62
- // ── By Layer ──
63
- const layers = ["presentation", "application", "data", "external", "deployment"];
33
+ // Layer counts
34
+ const layerNames = ["presentation", "application", "data", "external", "deployment"];
64
35
  const layerCounts = new Map();
65
36
  for (const node of realNodes) {
66
37
  layerCounts.set(node.layer, (layerCounts.get(node.layer) || 0) + 1);
67
38
  }
68
- const maxCount = Math.max(...layerCounts.values(), 1);
69
- console.log(chalk.bold(" By Layer:"));
70
- for (const layer of layers) {
39
+ const layers = {};
40
+ for (const layer of layerNames) {
71
41
  const count = layerCounts.get(layer) || 0;
72
- if (count === 0)
73
- continue;
74
- const barLen = Math.round((count / maxCount) * 10);
75
- const bar = "█".repeat(barLen) + "░".repeat(10 - barLen);
76
- const pct = realNodes.length > 0
77
- ? `(${Math.round((count / realNodes.length) * 100)}%)`
78
- : "";
79
- const label = (layer + " ").slice(0, 14);
80
- console.log(chalk.gray(` ${label}${bar} ${count} ${pct}`));
42
+ if (count > 0) {
43
+ layers[layer] = {
44
+ count,
45
+ percent: realNodes.length > 0 ? Math.round((count / realNodes.length) * 100) : 0,
46
+ };
47
+ }
81
48
  }
82
- console.log();
83
- // ── Health Indicators ──
84
- console.log(chalk.bold(" Health Indicators:"));
85
- // Connection density: edges / (n * (n-1) / 2) for undirected sense
49
+ // Health indicators
50
+ const health = [];
86
51
  const n = realNodes.length;
87
52
  const possibleConnections = n > 1 ? (n * (n - 1)) / 2 : 1;
88
53
  const density = totalConnections / possibleConnections;
89
- const densityStr = density.toFixed(2);
90
- if (density < 0.5) {
91
- console.log(chalk.green(` ✓ Connection density: ${densityStr} (healthy < 0.5)`));
92
- }
93
- else {
94
- console.log(chalk.yellow(`Connection density: ${densityStr} (threshold: 0.5)`));
95
- }
96
- // Hub detection (threshold: 5 connections)
54
+ health.push({
55
+ rule: "connection-density",
56
+ status: density < 0.5 ? "pass" : "warn",
57
+ message: density < 0.5
58
+ ? `Connection density: ${density.toFixed(2)} (healthy < 0.5)`
59
+ : `Connection density: ${density.toFixed(2)} (threshold: 0.5)`,
60
+ details: { density: parseFloat(density.toFixed(4)), threshold: 0.5 },
61
+ });
62
+ // Hub detection
97
63
  const connectionCounts = new Map();
98
64
  for (const edge of arch.edges) {
99
65
  connectionCounts.set(edge.source, (connectionCounts.get(edge.source) || 0) + 1);
100
66
  connectionCounts.set(edge.target, (connectionCounts.get(edge.target) || 0) + 1);
101
67
  }
102
68
  const hubThreshold = getThreshold(config, "max-connections", 6);
103
- let hubFound = false;
69
+ const hubs = [];
104
70
  for (const node of realNodes) {
105
71
  const count = connectionCounts.get(node.id) || 0;
106
72
  if (count >= hubThreshold) {
107
- console.log(chalk.yellow(` ⚠ Hub detected: "${node.label}" has ${count} connections (threshold: ${hubThreshold})`));
108
- hubFound = true;
73
+ hubs.push({ label: node.label, connections: count });
109
74
  }
110
75
  }
111
- if (!hubFound) {
112
- console.log(chalk.green(` ✓ No hubs detected (threshold: ${hubThreshold})`));
113
- }
76
+ health.push({
77
+ rule: "max-connections",
78
+ status: hubs.length === 0 ? "pass" : "warn",
79
+ message: hubs.length === 0
80
+ ? `No hubs detected (threshold: ${hubThreshold})`
81
+ : `${hubs.length} hub(s) detected (threshold: ${hubThreshold})`,
82
+ details: { threshold: hubThreshold, hubs },
83
+ });
114
84
  // Orphan detection
115
85
  const connectedIds = new Set();
116
86
  for (const edge of arch.edges) {
117
87
  connectedIds.add(edge.source);
118
88
  connectedIds.add(edge.target);
119
89
  }
120
- let orphanFound = false;
121
- for (const node of realNodes) {
122
- if (!connectedIds.has(node.id)) {
123
- console.log(chalk.yellow(` ⚠ Orphan found: "${node.label}" has 0 connections`));
124
- orphanFound = true;
125
- }
126
- }
127
- if (!orphanFound) {
128
- console.log(chalk.green(" ✓ No orphans detected"));
129
- }
130
- // Layer violations (presentation → data, skipping application)
90
+ const orphans = realNodes.filter((node) => !connectedIds.has(node.id)).map((n) => n.label);
91
+ health.push({
92
+ rule: "no-orphans",
93
+ status: orphans.length === 0 ? "pass" : "warn",
94
+ message: orphans.length === 0
95
+ ? "No orphans detected"
96
+ : `${orphans.length} orphan(s) found`,
97
+ details: { orphans },
98
+ });
99
+ // Layer violations
131
100
  const layerOrder = {
132
- presentation: 0,
133
- application: 1,
134
- data: 2,
135
- external: 3,
136
- deployment: 4,
101
+ presentation: 0, application: 1, data: 2, external: 3, deployment: 4,
137
102
  };
138
103
  const nodeMap = new Map();
139
- for (const node of realNodes) {
104
+ for (const node of realNodes)
140
105
  nodeMap.set(node.id, node);
141
- }
142
- let violationFound = false;
106
+ const violations = [];
143
107
  for (const edge of arch.edges) {
144
108
  const src = nodeMap.get(edge.source);
145
109
  const tgt = nodeMap.get(edge.target);
@@ -149,14 +113,106 @@ export async function handleStats(options) {
149
113
  const tgtOrder = layerOrder[tgt.layer];
150
114
  if (srcOrder === undefined || tgtOrder === undefined)
151
115
  continue;
152
- // Only check the core layers (presentation/application/data)
153
116
  if (srcOrder <= 1 && tgtOrder === 2 && tgtOrder - srcOrder > 1) {
154
- console.log(chalk.yellow(` ⚠ Layer violation: "${src.label}" (${src.layer}) "${tgt.label}" (${tgt.layer})`));
155
- violationFound = true;
117
+ violations.push({ from: src.label, to: tgt.label, fromLayer: src.layer, toLayer: tgt.layer });
118
+ }
119
+ }
120
+ health.push({
121
+ rule: "no-layer-bypass",
122
+ status: violations.length === 0 ? "pass" : "warn",
123
+ message: violations.length === 0
124
+ ? "No layer violations detected"
125
+ : `${violations.length} layer violation(s)`,
126
+ details: { violations },
127
+ });
128
+ return {
129
+ summary: {
130
+ components: components.length,
131
+ databases: databases.length,
132
+ externals: externals.length,
133
+ connections: totalConnections,
134
+ totalNodes: realNodes.length,
135
+ },
136
+ layers,
137
+ health,
138
+ ...(scanMeta.analyzedAt || scanMeta.durationMs ? { scan: scanMeta } : {}),
139
+ };
140
+ }
141
+ /**
142
+ * Print an architecture health dashboard to the terminal.
143
+ */
144
+ export async function handleStats(options) {
145
+ const stats = computeStats(options);
146
+ if (isJsonMode()) {
147
+ outputSuccess(stats);
148
+ return;
149
+ }
150
+ const projectName = process.cwd().split("/").pop() || "project";
151
+ console.log();
152
+ console.log(chalk.bold.cyan(`⚡ ArchByte Stats: ${projectName}`));
153
+ console.log();
154
+ // ── Scan Info ──
155
+ if (stats.scan) {
156
+ const parts = [];
157
+ if (stats.scan.durationMs != null)
158
+ parts.push(`${(stats.scan.durationMs / 1000).toFixed(1)}s`);
159
+ if (stats.scan.filesScanned != null)
160
+ parts.push(`${stats.scan.filesScanned} files scanned`);
161
+ if (stats.scan.tokenUsage) {
162
+ const total = stats.scan.tokenUsage.input + stats.scan.tokenUsage.output;
163
+ parts.push(`${total.toLocaleString()} tokens`);
164
+ }
165
+ if (stats.scan.mode)
166
+ parts.push(stats.scan.mode === "pipeline" ? "static + model" : "static only");
167
+ if (stats.scan.analyzedAt) {
168
+ const d = new Date(stats.scan.analyzedAt);
169
+ parts.push(d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" }));
170
+ }
171
+ if (parts.length > 0) {
172
+ console.log(chalk.gray(` Last scan: ${parts.join(" | ")}`));
173
+ console.log();
156
174
  }
157
175
  }
158
- if (!violationFound) {
159
- console.log(chalk.green(" ✓ No layer violations detected"));
176
+ // ── Summary ──
177
+ console.log(` Components: ${chalk.bold(String(stats.summary.components))} Connections: ${chalk.bold(String(stats.summary.connections))}`);
178
+ console.log(` Databases: ${chalk.bold(String(stats.summary.databases))} Ext Services: ${chalk.bold(String(stats.summary.externals))}`);
179
+ console.log();
180
+ // ── By Layer ──
181
+ const maxCount = Math.max(...Object.values(stats.layers).map((l) => l.count), 1);
182
+ console.log(chalk.bold(" By Layer:"));
183
+ for (const [layer, data] of Object.entries(stats.layers)) {
184
+ const barLen = Math.round((data.count / maxCount) * 10);
185
+ const bar = "█".repeat(barLen) + "░".repeat(10 - barLen);
186
+ const label = (layer + " ").slice(0, 14);
187
+ console.log(chalk.gray(` ${label}${bar} ${data.count} (${data.percent}%)`));
188
+ }
189
+ console.log();
190
+ // ── Health Indicators ──
191
+ console.log(chalk.bold(" Health Indicators:"));
192
+ for (const ind of stats.health) {
193
+ if (ind.status === "pass") {
194
+ console.log(chalk.green(` ✓ ${ind.message}`));
195
+ }
196
+ else {
197
+ console.log(chalk.yellow(` ⚠ ${ind.message}`));
198
+ // Print details for hubs, orphans, violations
199
+ const details = ind.details ?? {};
200
+ if (Array.isArray(details.hubs)) {
201
+ for (const h of details.hubs) {
202
+ console.log(chalk.yellow(` "${h.label}" has ${h.connections} connections`));
203
+ }
204
+ }
205
+ if (Array.isArray(details.orphans) && details.orphans.length > 0) {
206
+ for (const o of details.orphans) {
207
+ console.log(chalk.yellow(` "${o}" has 0 connections`));
208
+ }
209
+ }
210
+ if (Array.isArray(details.violations)) {
211
+ for (const v of details.violations) {
212
+ console.log(chalk.yellow(` "${v.from}" (${v.fromLayer}) → "${v.to}" (${v.toLayer})`));
213
+ }
214
+ }
215
+ }
160
216
  }
161
217
  console.log();
162
218
  }
package/dist/cli/ui.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import chalk from "chalk";
2
+ import { isJsonMode, isQuietMode } from "./output.js";
2
3
  const BRAILLE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
3
4
  // ─── Cursor Safety ───
4
5
  // Ensure the terminal cursor is always restored, even on unhandled crashes.
@@ -25,11 +26,15 @@ for (const event of ["exit", "SIGINT", "SIGTERM", "uncaughtException", "unhandle
25
26
  * Animated braille spinner. Falls back to static console.log when not a TTY.
26
27
  */
27
28
  export function spinner(label) {
28
- if (!process.stdout.isTTY) {
29
- console.log(` ${label}...`);
29
+ if (isJsonMode() || isQuietMode() || !process.stdout.isTTY) {
30
+ if (!isJsonMode() && !isQuietMode()) {
31
+ console.log(` ${label}...`);
32
+ }
30
33
  return {
31
34
  stop(result) {
32
- console.log(` ${label}... ${result}`);
35
+ if (!isJsonMode() && !isQuietMode()) {
36
+ console.log(` ${label}... ${result}`);
37
+ }
33
38
  },
34
39
  };
35
40
  }
@@ -55,10 +60,12 @@ export function spinner(label) {
55
60
  * Navigation: ↑/↓ arrows to move, Enter to confirm, Ctrl+C to exit.
56
61
  */
57
62
  export function select(prompt, options) {
58
- if (!process.stdout.isTTY || options.length === 0) {
59
- console.log(` ${prompt}`);
60
- if (options.length > 0)
61
- console.log(` → ${options[0]}`);
63
+ if (isJsonMode() || isQuietMode() || !process.stdout.isTTY || options.length === 0) {
64
+ if (!isJsonMode() && !isQuietMode()) {
65
+ console.log(` ${prompt}`);
66
+ if (options.length > 0)
67
+ console.log(` → ${options[0]}`);
68
+ }
62
69
  return Promise.resolve(0);
63
70
  }
64
71
  return new Promise((resolve) => {
@@ -138,16 +145,18 @@ export function progressBar(totalSteps) {
138
145
  function elapsed() {
139
146
  return ((Date.now() - startTime) / 1000).toFixed(1);
140
147
  }
141
- if (!process.stdout.isTTY) {
148
+ if (isJsonMode() || isQuietMode() || !process.stdout.isTTY) {
142
149
  return {
143
150
  update(_step, label) {
144
- if (label !== lastLabel) {
151
+ if (!isJsonMode() && !isQuietMode() && label !== lastLabel) {
145
152
  console.log(` [${elapsed()}s] ${label}`);
146
153
  lastLabel = label;
147
154
  }
148
155
  },
149
156
  done(label) {
150
- console.log(` [${elapsed()}s] ${label ?? "Done"}`);
157
+ if (!isJsonMode() && !isQuietMode()) {
158
+ console.log(` [${elapsed()}s] ${label ?? "Done"}`);
159
+ }
151
160
  },
152
161
  };
153
162
  }
@@ -179,8 +188,10 @@ export function progressBar(totalSteps) {
179
188
  * (arrow keys, etc.) to prevent accidental confirmation.
180
189
  */
181
190
  export function confirm(prompt) {
182
- if (!process.stdout.isTTY) {
183
- console.log(` ${prompt} (Y/n): y`);
191
+ if (isJsonMode() || isQuietMode() || !process.stdout.isTTY) {
192
+ if (!isJsonMode() && !isQuietMode()) {
193
+ console.log(` ${prompt} (Y/n): y`);
194
+ }
184
195
  return Promise.resolve(true);
185
196
  }
186
197
  return new Promise((resolve) => {
@@ -222,8 +233,10 @@ export function confirm(prompt) {
222
233
  * @param mask - If true, replaces each character with * (for passwords).
223
234
  */
224
235
  export function textInput(prompt, opts) {
225
- if (!process.stdout.isTTY) {
226
- console.log(` ${prompt}: `);
236
+ if (isJsonMode() || isQuietMode() || !process.stdout.isTTY) {
237
+ if (!isJsonMode() && !isQuietMode()) {
238
+ console.log(` ${prompt}: `);
239
+ }
227
240
  return Promise.resolve("");
228
241
  }
229
242
  return new Promise((resolve) => {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * ArchByte MCP Server
3
+ *
4
+ * Exposes architecture analysis tools and resources via the
5
+ * Model Context Protocol (stdio transport).
6
+ *
7
+ * Usage:
8
+ * archbyte mcp
9
+ *
10
+ * Agent config:
11
+ * { "mcpServers": { "archbyte": { "command": "npx", "args": ["archbyte", "mcp"] } } }
12
+ */
13
+ export declare function startMcpServer(version: string): Promise<void>;
@@ -0,0 +1,253 @@
1
+ /**
2
+ * ArchByte MCP Server
3
+ *
4
+ * Exposes architecture analysis tools and resources via the
5
+ * Model Context Protocol (stdio transport).
6
+ *
7
+ * Usage:
8
+ * archbyte mcp
9
+ *
10
+ * Agent config:
11
+ * { "mcpServers": { "archbyte": { "command": "npx", "args": ["archbyte", "mcp"] } } }
12
+ */
13
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
+ import { z } from "zod";
16
+ import * as path from "path";
17
+ // ─── Server Setup ───
18
+ export async function startMcpServer(version) {
19
+ const server = new McpServer({
20
+ name: "archbyte",
21
+ version,
22
+ });
23
+ // ─── Tools ───
24
+ server.tool("archbyte_analyze", "Run architecture analysis on a codebase. Returns component and connection counts, duration, and output path.", {
25
+ dir: z.string().optional().describe("Project root directory (default: cwd)"),
26
+ static: z.boolean().optional().describe("Static-only analysis (no model)"),
27
+ force: z.boolean().optional().describe("Force full re-scan"),
28
+ }, async (params) => {
29
+ try {
30
+ const { handleAnalyze } = await import("../cli/analyze.js");
31
+ const rootDir = params.dir ? path.resolve(params.dir) : process.cwd();
32
+ // Capture output by running analyze in quiet mode
33
+ const { setOutputMode } = await import("../cli/output.js");
34
+ setOutputMode({ json: true, quiet: true, command: "analyze", version });
35
+ // Run pipeline — it writes analysis.json and architecture.json
36
+ await handleAnalyze({
37
+ dir: rootDir,
38
+ static: params.static,
39
+ force: params.force,
40
+ skipServeHint: true,
41
+ });
42
+ // Read the results
43
+ const fs = await import("fs");
44
+ const analysisPath = path.join(rootDir, ".archbyte", "analysis.json");
45
+ if (fs.existsSync(analysisPath)) {
46
+ const analysis = JSON.parse(fs.readFileSync(analysisPath, "utf-8"));
47
+ const components = analysis.components?.length ?? 0;
48
+ const connections = analysis.connections?.length ?? 0;
49
+ return {
50
+ content: [
51
+ {
52
+ type: "text",
53
+ text: JSON.stringify({
54
+ outputPath: ".archbyte/analysis.json",
55
+ components,
56
+ connections,
57
+ mode: analysis.metadata?.mode ?? "unknown",
58
+ }, null, 2),
59
+ },
60
+ ],
61
+ };
62
+ }
63
+ return { content: [{ type: "text", text: "Analysis complete. Output: .archbyte/analysis.json" }] };
64
+ }
65
+ catch (err) {
66
+ return {
67
+ content: [{ type: "text", text: `Analysis failed: ${err instanceof Error ? err.message : String(err)}` }],
68
+ isError: true,
69
+ };
70
+ }
71
+ });
72
+ server.tool("archbyte_status", "Check ArchByte account status, tier, and usage.", {}, async () => {
73
+ try {
74
+ const { getAccountStatus } = await import("../cli/auth.js");
75
+ const status = await getAccountStatus();
76
+ if (!status) {
77
+ return {
78
+ content: [{ type: "text", text: JSON.stringify({ error: "Not logged in. Run: archbyte login" }) }],
79
+ isError: true,
80
+ };
81
+ }
82
+ return {
83
+ content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
84
+ };
85
+ }
86
+ catch (err) {
87
+ return {
88
+ content: [{ type: "text", text: `Status check failed: ${err instanceof Error ? err.message : String(err)}` }],
89
+ isError: true,
90
+ };
91
+ }
92
+ });
93
+ server.tool("archbyte_export", "Export architecture diagram to various formats (mermaid, json, markdown, plantuml, dot).", {
94
+ format: z.enum(["mermaid", "json", "markdown", "plantuml", "dot"]).optional().describe("Export format (default: mermaid)"),
95
+ diagram: z.string().optional().describe("Path to architecture.json"),
96
+ output: z.string().optional().describe("Write to file instead of returning content"),
97
+ }, async (params) => {
98
+ try {
99
+ const { generateExport } = await import("../cli/export.js");
100
+ const result = await generateExport({
101
+ format: params.format,
102
+ diagram: params.diagram,
103
+ output: params.output,
104
+ });
105
+ return {
106
+ content: [{ type: "text", text: result.content }],
107
+ };
108
+ }
109
+ catch (err) {
110
+ return {
111
+ content: [{ type: "text", text: `Export failed: ${err instanceof Error ? err.message : String(err)}` }],
112
+ isError: true,
113
+ };
114
+ }
115
+ });
116
+ server.tool("archbyte_stats", "Get architecture health metrics: component counts, layer distribution, and health indicators.", {
117
+ diagram: z.string().optional().describe("Path to architecture.json"),
118
+ config: z.string().optional().describe("Path to archbyte.yaml config"),
119
+ }, async (params) => {
120
+ try {
121
+ const { computeStats } = await import("../cli/stats.js");
122
+ const stats = computeStats({
123
+ diagram: params.diagram,
124
+ config: params.config,
125
+ });
126
+ return {
127
+ content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
128
+ };
129
+ }
130
+ catch (err) {
131
+ return {
132
+ content: [{ type: "text", text: `Stats failed: ${err instanceof Error ? err.message : String(err)}` }],
133
+ isError: true,
134
+ };
135
+ }
136
+ });
137
+ server.tool("archbyte_validate", "Run architecture fitness rules (connection density, hubs, orphans, layer violations).", {
138
+ diagram: z.string().optional().describe("Path to architecture.json"),
139
+ config: z.string().optional().describe("Path to archbyte.yaml config"),
140
+ }, async (params) => {
141
+ try {
142
+ const { computeStats } = await import("../cli/stats.js");
143
+ const stats = computeStats({
144
+ diagram: params.diagram,
145
+ config: params.config,
146
+ });
147
+ const failures = stats.health.filter((h) => h.status === "warn");
148
+ const passed = failures.length === 0;
149
+ return {
150
+ content: [
151
+ {
152
+ type: "text",
153
+ text: JSON.stringify({
154
+ valid: passed,
155
+ rules: stats.health.length,
156
+ passed: stats.health.filter((h) => h.status === "pass").length,
157
+ warnings: failures.length,
158
+ results: stats.health,
159
+ }, null, 2),
160
+ },
161
+ ],
162
+ };
163
+ }
164
+ catch (err) {
165
+ return {
166
+ content: [{ type: "text", text: `Validation failed: ${err instanceof Error ? err.message : String(err)}` }],
167
+ isError: true,
168
+ };
169
+ }
170
+ });
171
+ // ─── Resources ───
172
+ server.resource("Architecture", "archbyte://architecture", { description: "Full architecture state (nodes, edges, flows, environments)" }, async () => {
173
+ const data = await loadArchitectureData();
174
+ if (!data) {
175
+ return {
176
+ contents: [{
177
+ uri: "archbyte://architecture",
178
+ mimeType: "application/json",
179
+ text: JSON.stringify({ error: "No architecture.json found. Run: archbyte analyze" }),
180
+ }],
181
+ };
182
+ }
183
+ return {
184
+ contents: [{
185
+ uri: "archbyte://architecture",
186
+ mimeType: "application/json",
187
+ text: JSON.stringify(data, null, 2),
188
+ }],
189
+ };
190
+ });
191
+ server.resource("Components", "archbyte://components", { description: "Component list with types, layers, and tech stacks" }, async () => {
192
+ const data = await loadArchitectureData();
193
+ if (!data) {
194
+ return {
195
+ contents: [{
196
+ uri: "archbyte://components",
197
+ mimeType: "application/json",
198
+ text: JSON.stringify({ error: "No architecture.json found. Run: archbyte analyze" }),
199
+ }],
200
+ };
201
+ }
202
+ const components = data.nodes.map((n) => ({
203
+ id: n.id,
204
+ label: n.label,
205
+ type: n.type,
206
+ layer: n.layer,
207
+ techStack: n.techStack ?? [],
208
+ description: n.description,
209
+ }));
210
+ return {
211
+ contents: [{
212
+ uri: "archbyte://components",
213
+ mimeType: "application/json",
214
+ text: JSON.stringify(components, null, 2),
215
+ }],
216
+ };
217
+ });
218
+ server.resource("Connections", "archbyte://connections", { description: "Edge list with labels and environments" }, async () => {
219
+ const data = await loadArchitectureData();
220
+ if (!data) {
221
+ return {
222
+ contents: [{
223
+ uri: "archbyte://connections",
224
+ mimeType: "application/json",
225
+ text: JSON.stringify({ error: "No architecture.json found. Run: archbyte analyze" }),
226
+ }],
227
+ };
228
+ }
229
+ const connections = data.edges.map((e) => ({
230
+ id: e.id,
231
+ source: e.source,
232
+ target: e.target,
233
+ label: e.label,
234
+ environments: e.environments ?? [],
235
+ }));
236
+ return {
237
+ contents: [{
238
+ uri: "archbyte://connections",
239
+ mimeType: "application/json",
240
+ text: JSON.stringify(connections, null, 2),
241
+ }],
242
+ };
243
+ });
244
+ // ─── Start ───
245
+ const transport = new StdioServerTransport();
246
+ await server.connect(transport);
247
+ }
248
+ // ─── Helpers ───
249
+ async function loadArchitectureData() {
250
+ const { loadArchitectureFileSafe, resolveArchitecturePath } = await import("../cli/shared.js");
251
+ const archPath = resolveArchitecturePath({});
252
+ return loadArchitectureFileSafe(archPath);
253
+ }