archbyte 0.2.3 → 0.2.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.
- package/README.md +33 -5
- package/bin/archbyte.js +19 -1
- package/dist/cli/analyze.js +4 -3
- package/dist/cli/auth.js +6 -1
- package/dist/cli/config.js +5 -5
- package/dist/cli/diff.js +1 -1
- package/dist/cli/export.js +3 -3
- package/dist/cli/license-gate.js +2 -2
- package/dist/cli/mcp-server.d.ts +1 -0
- package/dist/cli/mcp-server.js +443 -0
- package/dist/cli/mcp.d.ts +1 -0
- package/dist/cli/mcp.js +102 -0
- package/dist/cli/patrol.js +4 -4
- package/dist/cli/setup.js +82 -3
- package/dist/cli/stats.js +1 -1
- package/dist/cli/ui.js +3 -3
- package/dist/cli/validate.js +2 -2
- package/dist/cli/workflow.js +7 -7
- package/dist/cli/yaml-io.js +1 -1
- package/dist/server/src/index.js +12 -0
- package/package.json +4 -2
- package/ui/dist/favicon-16.png +0 -0
- package/ui/dist/favicon-32.png +0 -0
- package/ui/dist/index.html +2 -1
package/README.md
CHANGED
|
@@ -6,21 +6,49 @@ AI-powered architecture visualization for your codebase. Analyzes code, generate
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
# Install
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
curl -fsSL https://archbyte.heartbyte.io/install.sh | bash
|
|
10
|
+
|
|
11
|
+
# Sign in or create a free account
|
|
12
|
+
archbyte login
|
|
13
|
+
|
|
14
|
+
# Configure your AI provider (BYOK)
|
|
15
|
+
archbyte init
|
|
16
|
+
|
|
17
|
+
# Or, if you use Claude Code / Codex — skip BYOK entirely:
|
|
18
|
+
archbyte mcp install
|
|
12
19
|
```
|
|
13
20
|
|
|
21
|
+
Your API keys stay on your machine. ArchByte never stores or transmits your provider credentials.
|
|
22
|
+
|
|
14
23
|
### Setup with Claude Code
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
**Option A: MCP (recommended)** — use ArchByte tools directly in Claude Code, no API key needed:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
archbyte mcp install
|
|
29
|
+
# or manually:
|
|
30
|
+
claude mcp add archbyte --transport stdio -- npx -y archbyte@latest mcp
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then just ask Claude Code in plain English:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
"Analyze the architecture of this project"
|
|
37
|
+
"Export the architecture as a Mermaid diagram"
|
|
38
|
+
"Show me the architecture stats"
|
|
39
|
+
"Read the current architecture diagram"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Available MCP tools: `archbyte_analyze`, `archbyte_generate`, `archbyte_validate` (Pro), `archbyte_export`, `archbyte_stats`, `archbyte_diff`, `archbyte_read_diagram`.
|
|
43
|
+
|
|
44
|
+
**Option B: Slash commands** — installed globally so they work in any project:
|
|
17
45
|
|
|
18
46
|
```bash
|
|
19
47
|
mkdir -p ~/.claude/commands
|
|
20
48
|
cp commands/archbyte-*.md ~/.claude/commands/
|
|
21
49
|
```
|
|
22
50
|
|
|
23
|
-
Run `/archbyte-help` in Claude Code to see all commands.
|
|
51
|
+
Run `/archbyte-help` in Claude Code to see all commands.
|
|
24
52
|
|
|
25
53
|
## Usage (Claude Code slash commands)
|
|
26
54
|
|
package/bin/archbyte.js
CHANGED
|
@@ -47,7 +47,7 @@ program
|
|
|
47
47
|
|
|
48
48
|
program
|
|
49
49
|
.command('login')
|
|
50
|
-
.description('Sign in
|
|
50
|
+
.description('Sign in or create a free account (required for scan/analyze/generate)')
|
|
51
51
|
.option('--token <jwt>', 'Login with a pre-existing JWT token')
|
|
52
52
|
.option('--github', 'Sign in with GitHub')
|
|
53
53
|
.option('--google', 'Sign in with Google')
|
|
@@ -236,6 +236,24 @@ program
|
|
|
236
236
|
await handleUpdate();
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
+
// — MCP server —
|
|
240
|
+
|
|
241
|
+
const mcpCmd = program
|
|
242
|
+
.command('mcp')
|
|
243
|
+
.description('Start MCP server for AI coding tools (Claude Code, Codex)')
|
|
244
|
+
.action(async () => {
|
|
245
|
+
const { startMcpServer } = await import('../dist/cli/mcp-server.js');
|
|
246
|
+
await startMcpServer();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
mcpCmd
|
|
250
|
+
.command('install')
|
|
251
|
+
.description('Auto-configure Claude Code and/or Codex CLI')
|
|
252
|
+
.action(async () => {
|
|
253
|
+
const { handleMcpInstall } = await import('../dist/cli/mcp.js');
|
|
254
|
+
await handleMcpInstall();
|
|
255
|
+
});
|
|
256
|
+
|
|
239
257
|
// Default: show help
|
|
240
258
|
program
|
|
241
259
|
.action(() => {
|
package/dist/cli/analyze.js
CHANGED
|
@@ -27,7 +27,7 @@ export async function handleAnalyze(options) {
|
|
|
27
27
|
const diagramPath = path.join(rootDir, ".archbyte", "architecture.json");
|
|
28
28
|
const diagramExists = fs.existsSync(diagramPath);
|
|
29
29
|
const outputPath = options.output ?? ".archbyte/analysis.json";
|
|
30
|
-
console.log(chalk.bold("ArchByte Analyze
|
|
30
|
+
console.log(chalk.bold("ArchByte Analyze: Dry Run"));
|
|
31
31
|
console.log(chalk.gray(` Project: ${path.basename(rootDir)}`));
|
|
32
32
|
console.log(chalk.gray(` Mode: ${mode}`));
|
|
33
33
|
console.log(chalk.gray(` Provider: ${provider}`));
|
|
@@ -123,7 +123,7 @@ export async function handleAnalyze(options) {
|
|
|
123
123
|
}
|
|
124
124
|
const { affected, unmapped } = mapFilesToComponents(changedFiles, priorSpec);
|
|
125
125
|
if (!shouldRunAgents(affected, unmapped)) {
|
|
126
|
-
console.log(chalk.green("Only config changes detected
|
|
126
|
+
console.log(chalk.green("Only config changes detected. No re-scan needed. Use --force to re-scan."));
|
|
127
127
|
console.log();
|
|
128
128
|
return;
|
|
129
129
|
}
|
|
@@ -319,8 +319,9 @@ function printSummary(analysis, durationMs, mode) {
|
|
|
319
319
|
console.log();
|
|
320
320
|
console.log(chalk.bold("Next steps:"));
|
|
321
321
|
console.log(chalk.gray(` ${chalk.cyan("archbyte serve")} -- view the architecture diagram`));
|
|
322
|
-
console.log(chalk.gray(` ${chalk.cyan("archbyte validate")} -- check architecture fitness rules`));
|
|
323
322
|
console.log(chalk.gray(` ${chalk.cyan("archbyte export")} -- export to mermaid, plantuml, etc.`));
|
|
323
|
+
console.log(chalk.gray(` ${chalk.cyan("archbyte validate")} -- check architecture fitness rules ${chalk.yellow("[Pro]")}`));
|
|
324
|
+
console.log(chalk.gray(` ${chalk.cyan("archbyte patrol")} -- continuous architecture monitoring ${chalk.yellow("[Pro]")}`));
|
|
324
325
|
console.log();
|
|
325
326
|
}
|
|
326
327
|
// ─── Analysis converters ───
|
package/dist/cli/auth.js
CHANGED
|
@@ -7,6 +7,7 @@ import { CONFIG_DIR, CREDENTIALS_PATH, API_BASE, CLI_CALLBACK_PORT, OAUTH_TIMEOU
|
|
|
7
7
|
export async function handleLogin(provider) {
|
|
8
8
|
console.log();
|
|
9
9
|
console.log(chalk.bold.cyan("ArchByte Login"));
|
|
10
|
+
console.log(chalk.gray("Sign in or create a free account to get started."));
|
|
10
11
|
console.log();
|
|
11
12
|
const existing = loadCredentials();
|
|
12
13
|
if (existing && !isExpired(existing)) {
|
|
@@ -33,7 +34,8 @@ export async function handleLogin(provider) {
|
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
const selectedProvider = provider ?? "github";
|
|
36
|
-
console.log(chalk.gray(`Opening browser for ${selectedProvider}
|
|
37
|
+
console.log(chalk.gray(`Opening browser for ${selectedProvider} sign-in...`));
|
|
38
|
+
console.log(chalk.gray("Don't have an account? One will be created automatically."));
|
|
37
39
|
console.log();
|
|
38
40
|
try {
|
|
39
41
|
const credentials = await startOAuthFlow(selectedProvider);
|
|
@@ -43,6 +45,9 @@ export async function handleLogin(provider) {
|
|
|
43
45
|
console.log();
|
|
44
46
|
console.log(chalk.green(`Logged in as ${chalk.bold(credentials.email)} (${credentials.tier} tier)`));
|
|
45
47
|
console.log(chalk.gray(`Credentials saved to ${CREDENTIALS_PATH}`));
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(chalk.gray("Your credentials and API keys are stored locally on your machine."));
|
|
50
|
+
console.log(chalk.gray("ArchByte never transmits or stores your model provider keys."));
|
|
46
51
|
}
|
|
47
52
|
catch (err) {
|
|
48
53
|
console.error(chalk.red(`Login failed: ${err instanceof Error ? err.message : "Unknown error"}`));
|
package/dist/cli/config.js
CHANGED
|
@@ -30,10 +30,10 @@ export async function handleConfig(options) {
|
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
console.error(chalk.red(`Unknown action: ${action}`));
|
|
33
|
-
console.error(chalk.gray(" archbyte config show
|
|
34
|
-
console.error(chalk.gray(" archbyte config set <k> <v>
|
|
35
|
-
console.error(chalk.gray(" archbyte config get <k>
|
|
36
|
-
console.error(chalk.gray(" archbyte config path
|
|
33
|
+
console.error(chalk.gray(" archbyte config show show current config"));
|
|
34
|
+
console.error(chalk.gray(" archbyte config set <k> <v> set a config value"));
|
|
35
|
+
console.error(chalk.gray(" archbyte config get <k> get a config value"));
|
|
36
|
+
console.error(chalk.gray(" archbyte config path show config file path"));
|
|
37
37
|
process.exit(1);
|
|
38
38
|
}
|
|
39
39
|
function loadConfig() {
|
|
@@ -105,7 +105,7 @@ function setConfig(key, value) {
|
|
|
105
105
|
console.log(chalk.green(`Switched to ${value} (credentials on file)`));
|
|
106
106
|
}
|
|
107
107
|
else {
|
|
108
|
-
console.log(chalk.yellow(`Switched to ${value}
|
|
108
|
+
console.log(chalk.yellow(`Switched to ${value}. Run archbyte init to configure API key.`));
|
|
109
109
|
}
|
|
110
110
|
break;
|
|
111
111
|
}
|
package/dist/cli/diff.js
CHANGED
|
@@ -17,7 +17,7 @@ export async function handleDiff(options) {
|
|
|
17
17
|
const currentArch = loadArchitectureFile(currentPath);
|
|
18
18
|
const projectName = process.cwd().split("/").pop() || "project";
|
|
19
19
|
console.log();
|
|
20
|
-
console.log(chalk.bold.cyan(`⚡ ArchByte Diff
|
|
20
|
+
console.log(chalk.bold.cyan(`⚡ ArchByte Diff: ${projectName}`));
|
|
21
21
|
console.log(chalk.gray(` Baseline: ${options.baseline}`));
|
|
22
22
|
console.log(chalk.gray(` Current: ${options.current || ".archbyte/architecture.json"}`));
|
|
23
23
|
console.log();
|
package/dist/cli/export.js
CHANGED
|
@@ -104,7 +104,7 @@ function exportMarkdown(arch) {
|
|
|
104
104
|
const realNodes = arch.nodes;
|
|
105
105
|
const projectName = process.cwd().split("/").pop() || "project";
|
|
106
106
|
const lines = [];
|
|
107
|
-
lines.push(`# Architecture
|
|
107
|
+
lines.push(`# Architecture: ${projectName}`);
|
|
108
108
|
lines.push("");
|
|
109
109
|
lines.push(`> Generated by ArchByte on ${new Date().toISOString().slice(0, 10)}`);
|
|
110
110
|
lines.push("");
|
|
@@ -127,7 +127,7 @@ function exportMarkdown(arch) {
|
|
|
127
127
|
for (const node of realNodes) {
|
|
128
128
|
const tech = node.techStack && node.techStack.length > 0
|
|
129
129
|
? node.techStack.join(", ")
|
|
130
|
-
: "
|
|
130
|
+
: "-";
|
|
131
131
|
lines.push(`| ${node.label} | ${node.type} | ${node.layer} | ${tech} |`);
|
|
132
132
|
}
|
|
133
133
|
lines.push("");
|
|
@@ -145,7 +145,7 @@ function exportMarkdown(arch) {
|
|
|
145
145
|
for (const edge of arch.edges) {
|
|
146
146
|
const src = nodeMap.get(edge.source)?.label || edge.source;
|
|
147
147
|
const tgt = nodeMap.get(edge.target)?.label || edge.target;
|
|
148
|
-
const label = edge.label ?
|
|
148
|
+
const label = edge.label ? `: ${edge.label}` : "";
|
|
149
149
|
lines.push(`- **${src}** → **${tgt}**${label}`);
|
|
150
150
|
}
|
|
151
151
|
}
|
package/dist/cli/license-gate.js
CHANGED
|
@@ -22,10 +22,10 @@ export async function requireLicense(action) {
|
|
|
22
22
|
console.error();
|
|
23
23
|
console.error(chalk.red("Authentication required."));
|
|
24
24
|
console.error();
|
|
25
|
-
console.error(chalk.gray("Sign in
|
|
25
|
+
console.error(chalk.gray("Sign in or create a free account:"));
|
|
26
26
|
console.error(chalk.gray(" archbyte login"));
|
|
27
27
|
console.error();
|
|
28
|
-
console.error(chalk.gray("
|
|
28
|
+
console.error(chalk.gray("Free tier includes unlimited scans. No credit card required."));
|
|
29
29
|
process.exit(1);
|
|
30
30
|
}
|
|
31
31
|
// Token expired locally
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startMcpServer(): Promise<void>;
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { loadCredentials, getVerifiedTier, cacheVerifiedTier, resetOfflineActions, checkOfflineAction, } from "./auth.js";
|
|
9
|
+
import { API_BASE, NETWORK_TIMEOUT_MS } from "./constants.js";
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
// At runtime, __dirname = dist/cli/ — project root is two levels up
|
|
12
|
+
const PROJECT_ROOT = path.resolve(__dirname, "..", "..");
|
|
13
|
+
const BIN_PATH = path.join(PROJECT_ROOT, "bin", "archbyte.js");
|
|
14
|
+
// Read version from package.json
|
|
15
|
+
function getVersion() {
|
|
16
|
+
try {
|
|
17
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf-8"));
|
|
18
|
+
return pkg.version ?? "0.0.0";
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return "0.0.0";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// ─── ANSI strip ───
|
|
25
|
+
const ANSI_RE = /\x1B\[[0-9;]*[A-Za-z]/g;
|
|
26
|
+
function stripAnsi(s) {
|
|
27
|
+
return s.replace(ANSI_RE, "");
|
|
28
|
+
}
|
|
29
|
+
// ─── Auth check ───
|
|
30
|
+
function checkAuth() {
|
|
31
|
+
const creds = loadCredentials();
|
|
32
|
+
if (!creds) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
error: "Not logged in. Run `archbyte login` to sign in.",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (new Date(creds.expiresAt) < new Date()) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
error: "Session expired. Run `archbyte login` to refresh.",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { ok: true };
|
|
45
|
+
}
|
|
46
|
+
// ─── License check ───
|
|
47
|
+
async function checkLicense(action) {
|
|
48
|
+
const creds = loadCredentials();
|
|
49
|
+
if (!creds) {
|
|
50
|
+
return {
|
|
51
|
+
allowed: false,
|
|
52
|
+
reason: "Not logged in. Run `archbyte login` to sign in.",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(`${API_BASE}/api/v1/check-usage`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
Authorization: `Bearer ${creds.token}`,
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify({ action }),
|
|
63
|
+
signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
|
|
64
|
+
});
|
|
65
|
+
if (res.status === 401) {
|
|
66
|
+
return {
|
|
67
|
+
allowed: false,
|
|
68
|
+
reason: "Session invalid. Run `archbyte login` again.",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
return handleOffline();
|
|
73
|
+
}
|
|
74
|
+
const data = (await res.json());
|
|
75
|
+
const tier = data.tier === "premium" ? "premium" : "free";
|
|
76
|
+
cacheVerifiedTier(tier, creds.email);
|
|
77
|
+
resetOfflineActions();
|
|
78
|
+
if (!data.allowed) {
|
|
79
|
+
return {
|
|
80
|
+
allowed: false,
|
|
81
|
+
reason: data.message ?? "Scan not allowed. Check your account status.",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return { allowed: true };
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return handleOffline();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function handleOffline() {
|
|
91
|
+
const { allowed, reason } = checkOfflineAction();
|
|
92
|
+
if (!allowed) {
|
|
93
|
+
return {
|
|
94
|
+
allowed: false,
|
|
95
|
+
reason: reason ??
|
|
96
|
+
"License server unreachable and offline actions not permitted.",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return { allowed: true };
|
|
100
|
+
}
|
|
101
|
+
function runArchbyte(args, cwd, timeoutMs = 300_000) {
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
const child = spawn(process.execPath, [BIN_PATH, ...args], {
|
|
104
|
+
cwd,
|
|
105
|
+
env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" },
|
|
106
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
107
|
+
timeout: timeoutMs,
|
|
108
|
+
});
|
|
109
|
+
const stdoutChunks = [];
|
|
110
|
+
const stderrChunks = [];
|
|
111
|
+
child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
112
|
+
child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
113
|
+
child.on("close", (code) => {
|
|
114
|
+
resolve({
|
|
115
|
+
stdout: stripAnsi(Buffer.concat(stdoutChunks).toString("utf-8")),
|
|
116
|
+
stderr: stripAnsi(Buffer.concat(stderrChunks).toString("utf-8")),
|
|
117
|
+
exitCode: code ?? 1,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
child.on("error", (err) => {
|
|
121
|
+
resolve({
|
|
122
|
+
stdout: "",
|
|
123
|
+
stderr: `Failed to spawn archbyte: ${err.message}`,
|
|
124
|
+
exitCode: 1,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// ─── Result helpers ───
|
|
130
|
+
function errorResult(msg) {
|
|
131
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
132
|
+
}
|
|
133
|
+
function textResult(text, isError = false) {
|
|
134
|
+
return { content: [{ type: "text", text }], isError };
|
|
135
|
+
}
|
|
136
|
+
function defineTools() {
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
tool: {
|
|
140
|
+
name: "archbyte_analyze",
|
|
141
|
+
description: "Run AI-powered architecture analysis on a codebase. Scans source code and produces a structured analysis of components, services, connections, and data flows. Requires a configured model provider (archbyte init) unless --static is used.",
|
|
142
|
+
inputSchema: {
|
|
143
|
+
type: "object",
|
|
144
|
+
properties: {
|
|
145
|
+
dir: {
|
|
146
|
+
type: "string",
|
|
147
|
+
description: "Project root directory (default: current directory)",
|
|
148
|
+
},
|
|
149
|
+
static: {
|
|
150
|
+
type: "boolean",
|
|
151
|
+
description: "Static-only analysis. No model needed, free.",
|
|
152
|
+
},
|
|
153
|
+
provider: {
|
|
154
|
+
type: "string",
|
|
155
|
+
description: "Model provider: anthropic, openai, google",
|
|
156
|
+
},
|
|
157
|
+
apiKey: {
|
|
158
|
+
type: "string",
|
|
159
|
+
description: "Model API key (overrides config)",
|
|
160
|
+
},
|
|
161
|
+
force: {
|
|
162
|
+
type: "boolean",
|
|
163
|
+
description: "Force full re-scan (skip incremental detection)",
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
minTier: "free",
|
|
169
|
+
licenseAction: "analyze",
|
|
170
|
+
handler: async (params) => {
|
|
171
|
+
const args = ["analyze"];
|
|
172
|
+
const cwd = params.dir ?? process.cwd();
|
|
173
|
+
if (params.dir)
|
|
174
|
+
args.push("--dir", params.dir);
|
|
175
|
+
if (params.static)
|
|
176
|
+
args.push("--static");
|
|
177
|
+
if (params.provider)
|
|
178
|
+
args.push("--provider", params.provider);
|
|
179
|
+
if (params.apiKey)
|
|
180
|
+
args.push("--api-key", params.apiKey);
|
|
181
|
+
if (params.force)
|
|
182
|
+
args.push("--force");
|
|
183
|
+
const result = await runArchbyte(args, cwd);
|
|
184
|
+
const output = (result.stdout + (result.stderr ? `\n${result.stderr}` : "")).trim();
|
|
185
|
+
return textResult(output || "Analysis complete.", result.exitCode !== 0);
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
tool: {
|
|
190
|
+
name: "archbyte_generate",
|
|
191
|
+
description: "Generate an interactive architecture diagram from analysis output. Reads analysis.json and produces architecture.json for the visualization UI.",
|
|
192
|
+
inputSchema: {
|
|
193
|
+
type: "object",
|
|
194
|
+
properties: {
|
|
195
|
+
input: {
|
|
196
|
+
type: "string",
|
|
197
|
+
description: "Input analysis path (default: .archbyte/analysis.json)",
|
|
198
|
+
},
|
|
199
|
+
dir: {
|
|
200
|
+
type: "string",
|
|
201
|
+
description: "Project root directory (default: current directory)",
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
minTier: "free",
|
|
207
|
+
licenseAction: "generate",
|
|
208
|
+
handler: async (params) => {
|
|
209
|
+
const args = ["generate"];
|
|
210
|
+
const cwd = params.dir ?? process.cwd();
|
|
211
|
+
if (params.input)
|
|
212
|
+
args.push("--input", params.input);
|
|
213
|
+
const result = await runArchbyte(args, cwd);
|
|
214
|
+
const output = (result.stdout + (result.stderr ? `\n${result.stderr}` : "")).trim();
|
|
215
|
+
return textResult(output || "Diagram generated.", result.exitCode !== 0);
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
tool: {
|
|
220
|
+
name: "archbyte_validate",
|
|
221
|
+
description: "Run architecture fitness function rules against the current diagram. Returns structured JSON results with pass/fail status for each rule. Useful for CI/CD pipelines and automated quality checks.",
|
|
222
|
+
inputSchema: {
|
|
223
|
+
type: "object",
|
|
224
|
+
properties: {
|
|
225
|
+
diagram: {
|
|
226
|
+
type: "string",
|
|
227
|
+
description: "Path to architecture JSON (default: .archbyte/architecture.json)",
|
|
228
|
+
},
|
|
229
|
+
config: {
|
|
230
|
+
type: "string",
|
|
231
|
+
description: "Path to archbyte.yaml config",
|
|
232
|
+
},
|
|
233
|
+
dir: {
|
|
234
|
+
type: "string",
|
|
235
|
+
description: "Project root directory (default: current directory)",
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
minTier: "premium",
|
|
241
|
+
licenseAction: "analyze",
|
|
242
|
+
handler: async (params) => {
|
|
243
|
+
const args = ["validate", "--ci"];
|
|
244
|
+
const cwd = params.dir ?? process.cwd();
|
|
245
|
+
if (params.diagram)
|
|
246
|
+
args.push("--diagram", params.diagram);
|
|
247
|
+
if (params.config)
|
|
248
|
+
args.push("--config", params.config);
|
|
249
|
+
const result = await runArchbyte(args, cwd);
|
|
250
|
+
const output = (result.stdout + (result.stderr ? `\n${result.stderr}` : "")).trim();
|
|
251
|
+
return textResult(output || "Validation complete.", result.exitCode !== 0);
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
tool: {
|
|
256
|
+
name: "archbyte_export",
|
|
257
|
+
description: "Export architecture diagram to various formats: mermaid, markdown, json, plantuml, or dot. Returns the exported content as text.",
|
|
258
|
+
inputSchema: {
|
|
259
|
+
type: "object",
|
|
260
|
+
properties: {
|
|
261
|
+
format: {
|
|
262
|
+
type: "string",
|
|
263
|
+
enum: ["mermaid", "markdown", "json", "plantuml", "dot"],
|
|
264
|
+
description: "Output format (default: mermaid)",
|
|
265
|
+
},
|
|
266
|
+
diagram: {
|
|
267
|
+
type: "string",
|
|
268
|
+
description: "Path to architecture JSON (default: .archbyte/architecture.json)",
|
|
269
|
+
},
|
|
270
|
+
dir: {
|
|
271
|
+
type: "string",
|
|
272
|
+
description: "Project root directory (default: current directory)",
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
minTier: "free",
|
|
278
|
+
licenseAction: null,
|
|
279
|
+
handler: async (params) => {
|
|
280
|
+
const args = ["export"];
|
|
281
|
+
const cwd = params.dir ?? process.cwd();
|
|
282
|
+
if (params.format)
|
|
283
|
+
args.push("--format", params.format);
|
|
284
|
+
if (params.diagram)
|
|
285
|
+
args.push("--diagram", params.diagram);
|
|
286
|
+
const result = await runArchbyte(args, cwd);
|
|
287
|
+
const output = (result.stdout + (result.stderr ? `\n${result.stderr}` : "")).trim();
|
|
288
|
+
return textResult(output || "Export complete.", result.exitCode !== 0);
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
tool: {
|
|
293
|
+
name: "archbyte_stats",
|
|
294
|
+
description: "Show architecture health dashboard with metrics: node count, edge count, complexity scores, and component breakdown.",
|
|
295
|
+
inputSchema: {
|
|
296
|
+
type: "object",
|
|
297
|
+
properties: {
|
|
298
|
+
diagram: {
|
|
299
|
+
type: "string",
|
|
300
|
+
description: "Path to architecture JSON (default: .archbyte/architecture.json)",
|
|
301
|
+
},
|
|
302
|
+
dir: {
|
|
303
|
+
type: "string",
|
|
304
|
+
description: "Project root directory (default: current directory)",
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
minTier: "free",
|
|
310
|
+
licenseAction: null,
|
|
311
|
+
handler: async (params) => {
|
|
312
|
+
const args = ["stats"];
|
|
313
|
+
const cwd = params.dir ?? process.cwd();
|
|
314
|
+
if (params.diagram)
|
|
315
|
+
args.push("--diagram", params.diagram);
|
|
316
|
+
const result = await runArchbyte(args, cwd);
|
|
317
|
+
const output = (result.stdout + (result.stderr ? `\n${result.stderr}` : "")).trim();
|
|
318
|
+
return textResult(output || "No stats available.", result.exitCode !== 0);
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
tool: {
|
|
323
|
+
name: "archbyte_diff",
|
|
324
|
+
description: "Compare two architecture snapshots to detect drift. Shows added, removed, and modified nodes and edges between a baseline and current diagram.",
|
|
325
|
+
inputSchema: {
|
|
326
|
+
type: "object",
|
|
327
|
+
properties: {
|
|
328
|
+
baseline: {
|
|
329
|
+
type: "string",
|
|
330
|
+
description: "Path to baseline architecture JSON",
|
|
331
|
+
},
|
|
332
|
+
current: {
|
|
333
|
+
type: "string",
|
|
334
|
+
description: "Path to current architecture JSON (default: .archbyte/architecture.json)",
|
|
335
|
+
},
|
|
336
|
+
dir: {
|
|
337
|
+
type: "string",
|
|
338
|
+
description: "Project root directory (default: current directory)",
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
required: ["baseline"],
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
minTier: "free",
|
|
345
|
+
licenseAction: null,
|
|
346
|
+
handler: async (params) => {
|
|
347
|
+
const args = ["diff", "--baseline", params.baseline];
|
|
348
|
+
const cwd = params.dir ?? process.cwd();
|
|
349
|
+
if (params.current)
|
|
350
|
+
args.push("--current", params.current);
|
|
351
|
+
const result = await runArchbyte(args, cwd);
|
|
352
|
+
const output = (result.stdout + (result.stderr ? `\n${result.stderr}` : "")).trim();
|
|
353
|
+
return textResult(output || "No differences found.", result.exitCode !== 0);
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
tool: {
|
|
358
|
+
name: "archbyte_read_diagram",
|
|
359
|
+
description: "Read the current architecture diagram JSON. Returns the full architecture.json content including all nodes, edges, and metadata. Useful for understanding the project structure without re-running analysis.",
|
|
360
|
+
inputSchema: {
|
|
361
|
+
type: "object",
|
|
362
|
+
properties: {
|
|
363
|
+
diagram: {
|
|
364
|
+
type: "string",
|
|
365
|
+
description: "Path to architecture JSON (default: .archbyte/architecture.json)",
|
|
366
|
+
},
|
|
367
|
+
dir: {
|
|
368
|
+
type: "string",
|
|
369
|
+
description: "Project root directory (default: current directory)",
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
minTier: "free",
|
|
375
|
+
licenseAction: null,
|
|
376
|
+
handler: async (params) => {
|
|
377
|
+
const cwd = params.dir ?? process.cwd();
|
|
378
|
+
const diagramPath = params.diagram ??
|
|
379
|
+
path.join(cwd, ".archbyte", "architecture.json");
|
|
380
|
+
const resolved = path.isAbsolute(diagramPath)
|
|
381
|
+
? diagramPath
|
|
382
|
+
: path.join(cwd, diagramPath);
|
|
383
|
+
try {
|
|
384
|
+
const content = fs.readFileSync(resolved, "utf-8");
|
|
385
|
+
return textResult(content);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return errorResult(`Diagram not found at ${resolved}. Run \`archbyte analyze\` then \`archbyte generate\` first.`);
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
];
|
|
393
|
+
}
|
|
394
|
+
// ─── Tier comparison ───
|
|
395
|
+
const TIER_RANK = { free: 0, premium: 1 };
|
|
396
|
+
function tierSatisfies(userTier, required) {
|
|
397
|
+
return TIER_RANK[userTier] >= TIER_RANK[required];
|
|
398
|
+
}
|
|
399
|
+
// ─── Start MCP server ───
|
|
400
|
+
export async function startMcpServer() {
|
|
401
|
+
const allTools = defineTools();
|
|
402
|
+
const toolMap = new Map(allTools.map((t) => [t.tool.name, t]));
|
|
403
|
+
const server = new Server({ name: "archbyte", version: getVersion() }, { capabilities: { tools: { listChanged: true } } });
|
|
404
|
+
// ─── tools/list — filtered by user tier ───
|
|
405
|
+
// Always show free-tier tools (even when not logged in) so the AI agent
|
|
406
|
+
// can attempt to call them and receive the "please login" error message.
|
|
407
|
+
// Premium tools are only shown to premium users.
|
|
408
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
409
|
+
const auth = checkAuth();
|
|
410
|
+
const userTier = auth.ok ? getVerifiedTier() : "free";
|
|
411
|
+
const visible = allTools
|
|
412
|
+
.filter((t) => tierSatisfies(userTier, t.minTier))
|
|
413
|
+
.map((t) => t.tool);
|
|
414
|
+
return { tools: visible };
|
|
415
|
+
});
|
|
416
|
+
// ─── tools/call — auth + license + execute ───
|
|
417
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
418
|
+
const { name, arguments: args } = request.params;
|
|
419
|
+
const def = toolMap.get(name);
|
|
420
|
+
if (!def) {
|
|
421
|
+
return errorResult(`Unknown tool: ${name}`);
|
|
422
|
+
}
|
|
423
|
+
// Auth check
|
|
424
|
+
const auth = checkAuth();
|
|
425
|
+
if (!auth.ok)
|
|
426
|
+
return errorResult(auth.error);
|
|
427
|
+
// Tier check (defense in depth — tool might not be in list but called directly)
|
|
428
|
+
const userTier = getVerifiedTier();
|
|
429
|
+
if (!tierSatisfies(userTier, def.minTier)) {
|
|
430
|
+
return errorResult(`${name} requires a Pro subscription. Upgrade at https://archbyte.heartbyte.io`);
|
|
431
|
+
}
|
|
432
|
+
// License check for gated tools
|
|
433
|
+
if (def.licenseAction) {
|
|
434
|
+
const license = await checkLicense(def.licenseAction);
|
|
435
|
+
if (!license.allowed)
|
|
436
|
+
return errorResult(license.reason);
|
|
437
|
+
}
|
|
438
|
+
return def.handler(args ?? {});
|
|
439
|
+
});
|
|
440
|
+
// ─── Connect transport ───
|
|
441
|
+
const transport = new StdioServerTransport();
|
|
442
|
+
await server.connect(transport);
|
|
443
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleMcpInstall(): Promise<void>;
|
package/dist/cli/mcp.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { execSync, spawnSync } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
function isInPath(cmd) {
|
|
6
|
+
try {
|
|
7
|
+
execSync(`which ${cmd}`, { stdio: "ignore" });
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function handleMcpInstall() {
|
|
15
|
+
console.log();
|
|
16
|
+
console.log(chalk.bold.cyan("ArchByte MCP Setup"));
|
|
17
|
+
console.log();
|
|
18
|
+
let configured = false;
|
|
19
|
+
// ─── Claude Code ───
|
|
20
|
+
if (isInPath("claude")) {
|
|
21
|
+
console.log(chalk.white("Detected Claude Code."));
|
|
22
|
+
const result = spawnSync("claude", ["mcp", "add", "archbyte", "--transport", "stdio", "--", "npx", "-y", "archbyte@latest", "mcp"], { stdio: "inherit" });
|
|
23
|
+
if (result.status === 0) {
|
|
24
|
+
console.log(chalk.green(" Configured Claude Code MCP server."));
|
|
25
|
+
configured = true;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.log(chalk.yellow(" Failed to configure Claude Code. See error above."));
|
|
29
|
+
}
|
|
30
|
+
console.log();
|
|
31
|
+
}
|
|
32
|
+
// ─── Codex CLI ───
|
|
33
|
+
const codexDir = path.join(process.env.HOME ?? process.env.USERPROFILE ?? ".", ".codex");
|
|
34
|
+
const codexConfig = path.join(codexDir, "config.toml");
|
|
35
|
+
if (fs.existsSync(codexDir)) {
|
|
36
|
+
console.log(chalk.white("Detected Codex CLI."));
|
|
37
|
+
let existing = "";
|
|
38
|
+
try {
|
|
39
|
+
existing = fs.readFileSync(codexConfig, "utf-8");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// File doesn't exist yet — we'll create it
|
|
43
|
+
}
|
|
44
|
+
if (existing.includes("[mcp_servers.archbyte]")) {
|
|
45
|
+
console.log(chalk.gray(" Codex already configured for archbyte."));
|
|
46
|
+
configured = true;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const block = `
|
|
50
|
+
[mcp_servers.archbyte]
|
|
51
|
+
type = "stdio"
|
|
52
|
+
command = "npx"
|
|
53
|
+
args = ["-y", "archbyte@latest", "mcp"]
|
|
54
|
+
`;
|
|
55
|
+
fs.appendFileSync(codexConfig, block, "utf-8");
|
|
56
|
+
console.log(chalk.green(" Configured Codex CLI MCP server."));
|
|
57
|
+
configured = true;
|
|
58
|
+
}
|
|
59
|
+
console.log();
|
|
60
|
+
}
|
|
61
|
+
// ─── Fallback instructions ───
|
|
62
|
+
if (!configured) {
|
|
63
|
+
console.log(chalk.yellow("No supported AI coding tool detected."));
|
|
64
|
+
console.log();
|
|
65
|
+
}
|
|
66
|
+
// ─── Usage examples ───
|
|
67
|
+
if (configured) {
|
|
68
|
+
console.log(chalk.bold("What to do next:"));
|
|
69
|
+
console.log();
|
|
70
|
+
console.log(chalk.white(" Open your AI coding tool and ask it to use ArchByte. For example:"));
|
|
71
|
+
console.log();
|
|
72
|
+
console.log(chalk.cyan(' "Analyze the architecture of this project"'));
|
|
73
|
+
console.log(chalk.cyan(' "Export the architecture as a Mermaid diagram"'));
|
|
74
|
+
console.log(chalk.cyan(' "Show me the architecture stats"'));
|
|
75
|
+
console.log(chalk.cyan(' "Read the current architecture diagram"'));
|
|
76
|
+
console.log();
|
|
77
|
+
console.log(chalk.gray(" Your AI tool will call ArchByte tools automatically via MCP."));
|
|
78
|
+
console.log(chalk.gray(" No commands to memorize. Just describe what you need."));
|
|
79
|
+
console.log();
|
|
80
|
+
}
|
|
81
|
+
console.log(chalk.bold("Manual setup:"));
|
|
82
|
+
console.log();
|
|
83
|
+
console.log(chalk.white(" Claude Code:"));
|
|
84
|
+
console.log(chalk.gray(" claude mcp add archbyte --transport stdio -- npx -y archbyte@latest mcp"));
|
|
85
|
+
console.log();
|
|
86
|
+
console.log(chalk.white(" Codex CLI:"));
|
|
87
|
+
console.log(chalk.gray(" Add to ~/.codex/config.toml:"));
|
|
88
|
+
console.log(chalk.gray(' [mcp_servers.archbyte]'));
|
|
89
|
+
console.log(chalk.gray(' type = "stdio"'));
|
|
90
|
+
console.log(chalk.gray(' command = "npx"'));
|
|
91
|
+
console.log(chalk.gray(' args = ["-y", "archbyte@latest", "mcp"]'));
|
|
92
|
+
console.log();
|
|
93
|
+
console.log(chalk.bold(" Available MCP tools:"));
|
|
94
|
+
console.log(chalk.gray(" archbyte_analyze Scan codebase with AI agents"));
|
|
95
|
+
console.log(chalk.gray(" archbyte_generate Generate diagram from analysis"));
|
|
96
|
+
console.log(chalk.gray(" archbyte_validate Run fitness rules (Pro)"));
|
|
97
|
+
console.log(chalk.gray(" archbyte_export Export to Mermaid, PlantUML, DOT, etc."));
|
|
98
|
+
console.log(chalk.gray(" archbyte_stats Architecture health metrics"));
|
|
99
|
+
console.log(chalk.gray(" archbyte_diff Compare snapshots, detect drift"));
|
|
100
|
+
console.log(chalk.gray(" archbyte_read_diagram Read current architecture JSON"));
|
|
101
|
+
console.log();
|
|
102
|
+
}
|
package/dist/cli/patrol.js
CHANGED
|
@@ -106,7 +106,7 @@ function printPatrolResult(record, cycleNum) {
|
|
|
106
106
|
const time = new Date(record.timestamp).toLocaleTimeString();
|
|
107
107
|
const status = record.passed ? chalk.green("HEALTHY") : chalk.red("VIOLATION");
|
|
108
108
|
console.log();
|
|
109
|
-
console.log(chalk.bold.cyan(` Patrol #${cycleNum}
|
|
109
|
+
console.log(chalk.bold.cyan(` Patrol #${cycleNum} | ${time} | ${status}`));
|
|
110
110
|
if (record.newViolations.length > 0) {
|
|
111
111
|
console.log(chalk.red(` ${record.newViolations.length} new violation(s):`));
|
|
112
112
|
for (const v of record.newViolations) {
|
|
@@ -121,7 +121,7 @@ function printPatrolResult(record, cycleNum) {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
if (record.newViolations.length === 0 && record.resolvedViolations.length === 0) {
|
|
124
|
-
console.log(chalk.gray(` No changes
|
|
124
|
+
console.log(chalk.gray(` No changes. ${record.errors} errors, ${record.warnings} warnings`));
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
function printHistory(patrolDir) {
|
|
@@ -137,7 +137,7 @@ function printHistory(patrolDir) {
|
|
|
137
137
|
}
|
|
138
138
|
const projectName = process.cwd().split("/").pop() || "project";
|
|
139
139
|
console.log();
|
|
140
|
-
console.log(chalk.bold.cyan(` ArchByte Patrol History
|
|
140
|
+
console.log(chalk.bold.cyan(` ArchByte Patrol History: ${projectName}`));
|
|
141
141
|
console.log();
|
|
142
142
|
// Show last 20 records (skip malformed lines)
|
|
143
143
|
const records = lines.slice(-20).flatMap((l) => {
|
|
@@ -208,7 +208,7 @@ export async function handlePatrol(options) {
|
|
|
208
208
|
const intervalStr = options.interval || "5m";
|
|
209
209
|
const action = options.onViolation || "log";
|
|
210
210
|
console.log();
|
|
211
|
-
console.log(chalk.bold.cyan(` ArchByte Patrol
|
|
211
|
+
console.log(chalk.bold.cyan(` ArchByte Patrol: ${projectName}`));
|
|
212
212
|
console.log(chalk.gray(` Interval: ${intervalStr} | On violation: ${action}`));
|
|
213
213
|
console.log(chalk.gray(` History: ${path.join(PATROL_DIR, HISTORY_FILE)}`));
|
|
214
214
|
console.log(chalk.gray(" Press Ctrl+C to stop."));
|
package/dist/cli/setup.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
+
import { execSync } from "child_process";
|
|
4
5
|
import chalk from "chalk";
|
|
5
6
|
import { resolveModel } from "../agents/runtime/types.js";
|
|
6
7
|
import { createProvider } from "../agents/providers/router.js";
|
|
@@ -111,10 +112,48 @@ async function validateProviderSilent(providerName, apiKey) {
|
|
|
111
112
|
function getProfiles(config) {
|
|
112
113
|
return config.profiles ?? {};
|
|
113
114
|
}
|
|
115
|
+
function isInPath(cmd) {
|
|
116
|
+
try {
|
|
117
|
+
execSync(`which ${cmd}`, { stdio: "ignore" });
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
114
124
|
export async function handleSetup() {
|
|
115
125
|
console.log();
|
|
116
126
|
console.log(chalk.bold.cyan("ArchByte Setup"));
|
|
117
127
|
console.log(chalk.gray("Configure your model provider and API key.\n"));
|
|
128
|
+
// Detect AI coding tools — suggest MCP instead of BYOK
|
|
129
|
+
const hasClaude = isInPath("claude");
|
|
130
|
+
const codexDir = path.join(process.env.HOME ?? process.env.USERPROFILE ?? ".", ".codex");
|
|
131
|
+
const hasCodex = fs.existsSync(codexDir);
|
|
132
|
+
if (hasClaude || hasCodex) {
|
|
133
|
+
const tools = [hasClaude && "Claude Code", hasCodex && "Codex CLI"].filter(Boolean).join(" and ");
|
|
134
|
+
console.log(chalk.cyan(` Detected ${tools} on this machine.`));
|
|
135
|
+
console.log(chalk.white(` You can use ArchByte directly through MCP. No API key needed.`));
|
|
136
|
+
console.log(chalk.white(` Your AI tool already provides the model, so you skip the BYOK step.`));
|
|
137
|
+
console.log();
|
|
138
|
+
console.log(chalk.white(` Run: `) + chalk.bold.cyan(`archbyte mcp install`));
|
|
139
|
+
console.log();
|
|
140
|
+
const continueIdx = await select("Continue with BYOK setup anyway?", [
|
|
141
|
+
`Skip ${chalk.gray("(use MCP instead, recommended)")}`,
|
|
142
|
+
`Continue ${chalk.gray("(set up your own API key)")}`,
|
|
143
|
+
]);
|
|
144
|
+
if (continueIdx === 0) {
|
|
145
|
+
console.log();
|
|
146
|
+
console.log(chalk.gray(" Run `archbyte mcp install` to configure MCP for your AI tool."));
|
|
147
|
+
console.log();
|
|
148
|
+
console.log(chalk.gray(" Then open your AI tool and try:"));
|
|
149
|
+
console.log(chalk.cyan(' "Analyze the architecture of this project"'));
|
|
150
|
+
console.log(chalk.cyan(' "Export the architecture as a Mermaid diagram"'));
|
|
151
|
+
console.log(chalk.cyan(' "Show me the architecture stats"'));
|
|
152
|
+
console.log();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
console.log();
|
|
156
|
+
}
|
|
118
157
|
const config = loadConfig();
|
|
119
158
|
const profiles = getProfiles(config);
|
|
120
159
|
// Migrate legacy flat config → profiles
|
|
@@ -151,7 +190,7 @@ export async function handleSetup() {
|
|
|
151
190
|
const idx = await select("Choose your model provider:", PROVIDERS.map((p) => {
|
|
152
191
|
const active = config.provider === p.name ? chalk.green(" (active)") : "";
|
|
153
192
|
const hasKey = profiles[p.name]?.apiKey ? chalk.cyan(" ✓ configured") : "";
|
|
154
|
-
return `${p.label} ${chalk.gray(
|
|
193
|
+
return `${p.label} ${chalk.gray(p.hint)}${hasKey}${active}`;
|
|
155
194
|
}));
|
|
156
195
|
const provider = PROVIDERS[idx].name;
|
|
157
196
|
config.provider = provider;
|
|
@@ -163,7 +202,7 @@ export async function handleSetup() {
|
|
|
163
202
|
if (existing?.apiKey) {
|
|
164
203
|
console.log(chalk.gray(`\n Existing key: ${maskKey(existing.apiKey)}`));
|
|
165
204
|
const keepIdx = await select(" Update API key?", [
|
|
166
|
-
`Keep existing ${chalk.gray("
|
|
205
|
+
`Keep existing ${chalk.gray("(" + maskKey(existing.apiKey) + ")")}`,
|
|
167
206
|
"Enter new key",
|
|
168
207
|
]);
|
|
169
208
|
if (keepIdx === 0) {
|
|
@@ -199,7 +238,7 @@ export async function handleSetup() {
|
|
|
199
238
|
const currentModel = profiles[provider].model;
|
|
200
239
|
const modelIdx = await select("\n Choose a model:", models.map((m) => {
|
|
201
240
|
const current = currentModel === m.id && m.id ? chalk.green(" (current)") : "";
|
|
202
|
-
return `${m.label} ${chalk.gray(
|
|
241
|
+
return `${m.label} ${chalk.gray(m.hint)}${current}`;
|
|
203
242
|
}));
|
|
204
243
|
const chosen = models[modelIdx];
|
|
205
244
|
if (chosen.id) {
|
|
@@ -243,6 +282,8 @@ export async function handleSetup() {
|
|
|
243
282
|
// Save config
|
|
244
283
|
saveConfig(config);
|
|
245
284
|
console.log(chalk.gray(`\n Config saved to ${CONFIG_PATH}`));
|
|
285
|
+
console.log(chalk.gray(" Your API key is stored locally on this machine and never sent to ArchByte."));
|
|
286
|
+
console.log(chalk.gray(" All model calls go directly from your machine to your provider."));
|
|
246
287
|
// Generate archbyte.yaml in .archbyte/ if it doesn't exist
|
|
247
288
|
const projectDir = process.cwd();
|
|
248
289
|
const archbyteDir = path.join(projectDir, ".archbyte");
|
|
@@ -271,6 +312,8 @@ export async function handleSetup() {
|
|
|
271
312
|
else {
|
|
272
313
|
console.log(chalk.gray(` .archbyte/archbyte.yaml already exists`));
|
|
273
314
|
}
|
|
315
|
+
// Generate README.md in .archbyte/
|
|
316
|
+
writeArchbyteReadme(archbyteDir);
|
|
274
317
|
console.log();
|
|
275
318
|
console.log(chalk.green(" Setup complete!"));
|
|
276
319
|
console.log();
|
|
@@ -278,3 +321,39 @@ export async function handleSetup() {
|
|
|
278
321
|
console.log(chalk.gray(" Run from your project root, or use ") + chalk.cyan("archbyte run -d /path/to/project"));
|
|
279
322
|
console.log();
|
|
280
323
|
}
|
|
324
|
+
function writeArchbyteReadme(archbyteDir) {
|
|
325
|
+
const readmePath = path.join(archbyteDir, "README.md");
|
|
326
|
+
if (fs.existsSync(readmePath))
|
|
327
|
+
return;
|
|
328
|
+
const content = `# .archbyte
|
|
329
|
+
|
|
330
|
+
This directory is managed by [ArchByte](https://diabhey.com) — your codebase's architecture intelligence layer.
|
|
331
|
+
|
|
332
|
+
## Files
|
|
333
|
+
|
|
334
|
+
| File | What it does |
|
|
335
|
+
|------|-------------|
|
|
336
|
+
| \`archbyte.yaml\` | Your architecture spec. Human-readable, human-editable. This is the source of truth. |
|
|
337
|
+
| \`architecture.json\` | Generated diagram data. Fed directly to the interactive UI. |
|
|
338
|
+
| \`analysis.json\` | Raw analysis output from the agent pipeline. Used by \`archbyte generate\`. |
|
|
339
|
+
| \`metadata.json\` | Scan metadata — duration, mode, commit hash. Powers \`archbyte stats\` and \`archbyte diff\`. |
|
|
340
|
+
| \`analysis-status.json\` | Status of the last scan (success/error). Used by the dev server for live updates. |
|
|
341
|
+
| \`static-context.json\` | Cached static scanner output. Speeds up incremental re-scans. |
|
|
342
|
+
|
|
343
|
+
## Workflow
|
|
344
|
+
|
|
345
|
+
\`\`\`
|
|
346
|
+
archbyte analyze → writes archbyte.yaml + analysis.json
|
|
347
|
+
archbyte generate → reads archbyte.yaml → writes architecture.json
|
|
348
|
+
archbyte serve → reads architecture.json → opens interactive diagram
|
|
349
|
+
\`\`\`
|
|
350
|
+
|
|
351
|
+
You can edit \`archbyte.yaml\` by hand — your changes are preserved on re-scan.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
Thanks for using ArchByte. Built with love by [diabhey](https://diabhey.com).
|
|
356
|
+
`;
|
|
357
|
+
fs.writeFileSync(readmePath, content, "utf-8");
|
|
358
|
+
console.log(chalk.green(` Created .archbyte/README.md`));
|
|
359
|
+
}
|
package/dist/cli/stats.js
CHANGED
|
@@ -17,7 +17,7 @@ export async function handleStats(options) {
|
|
|
17
17
|
// Auto-detect project name from cwd
|
|
18
18
|
const projectName = process.cwd().split("/").pop() || "project";
|
|
19
19
|
console.log();
|
|
20
|
-
console.log(chalk.bold.cyan(`⚡ ArchByte Stats
|
|
20
|
+
console.log(chalk.bold.cyan(`⚡ ArchByte Stats: ${projectName}`));
|
|
21
21
|
console.log();
|
|
22
22
|
// ── Scan Info (from metadata.json, fallback to analysis.json) ──
|
|
23
23
|
const metadataJsonPath = path.join(process.cwd(), ".archbyte", "metadata.json");
|
package/dist/cli/ui.js
CHANGED
|
@@ -89,8 +89,8 @@ export function select(prompt, options) {
|
|
|
89
89
|
cleanup();
|
|
90
90
|
resolve(selected);
|
|
91
91
|
}
|
|
92
|
-
else if (data === "\x03") {
|
|
93
|
-
// Ctrl+C
|
|
92
|
+
else if (data === "\x03" || data === "q" || data === "Q") {
|
|
93
|
+
// Ctrl+C or q to quit
|
|
94
94
|
cleanup();
|
|
95
95
|
process.exit(0);
|
|
96
96
|
}
|
|
@@ -173,7 +173,7 @@ export function confirm(prompt) {
|
|
|
173
173
|
process.stdout.write("n\n");
|
|
174
174
|
resolve(false);
|
|
175
175
|
}
|
|
176
|
-
else if (data === "\x03") {
|
|
176
|
+
else if (data === "\x03" || data === "q" || data === "Q") {
|
|
177
177
|
process.stdout.write("\n");
|
|
178
178
|
process.exit(0);
|
|
179
179
|
}
|
package/dist/cli/validate.js
CHANGED
|
@@ -225,7 +225,7 @@ export async function handleValidate(options) {
|
|
|
225
225
|
function printValidationReport(result) {
|
|
226
226
|
const projectName = process.cwd().split("/").pop() || "project";
|
|
227
227
|
console.log();
|
|
228
|
-
console.log(chalk.bold.cyan(`⚡ ArchByte Validate
|
|
228
|
+
console.log(chalk.bold.cyan(`⚡ ArchByte Validate: ${projectName}`));
|
|
229
229
|
console.log();
|
|
230
230
|
// Group violations by rule for display
|
|
231
231
|
const ruleViolations = new Map();
|
|
@@ -248,7 +248,7 @@ function printValidationReport(result) {
|
|
|
248
248
|
}
|
|
249
249
|
console.log();
|
|
250
250
|
const resultStr = result.errors > 0 ? chalk.red("FAIL") : chalk.green("PASS");
|
|
251
|
-
console.log(` Result: ${result.warnings} warning${result.warnings !== 1 ? "s" : ""}, ${result.errors} error${result.errors !== 1 ? "s" : ""}
|
|
251
|
+
console.log(` Result: ${result.warnings} warning${result.warnings !== 1 ? "s" : ""}, ${result.errors} error${result.errors !== 1 ? "s" : ""} ${resultStr}`);
|
|
252
252
|
console.log();
|
|
253
253
|
}
|
|
254
254
|
function printRuleResult(rule, level, count, violations) {
|
package/dist/cli/workflow.js
CHANGED
|
@@ -349,7 +349,7 @@ async function runWorkflow(workflow) {
|
|
|
349
349
|
// Check dependencies
|
|
350
350
|
const unmetDeps = step.needs.filter((dep) => state.steps[dep]?.status !== "completed");
|
|
351
351
|
if (unmetDeps.length > 0) {
|
|
352
|
-
console.log(chalk.yellow(` [skip] ${step.name}
|
|
352
|
+
console.log(chalk.yellow(` [skip] ${step.name} (waiting on: ${unmetDeps.join(", ")})`));
|
|
353
353
|
state.steps[step.id] = { status: "pending" };
|
|
354
354
|
saveState(state);
|
|
355
355
|
continue;
|
|
@@ -414,7 +414,7 @@ function listWorkflows() {
|
|
|
414
414
|
const workflows = loadWorkflows();
|
|
415
415
|
const projectName = process.cwd().split("/").pop() || "project";
|
|
416
416
|
console.log();
|
|
417
|
-
console.log(chalk.bold.cyan(` ArchByte Workflows
|
|
417
|
+
console.log(chalk.bold.cyan(` ArchByte Workflows: ${projectName}`));
|
|
418
418
|
console.log();
|
|
419
419
|
for (const w of workflows) {
|
|
420
420
|
const state = loadState(w.id);
|
|
@@ -428,7 +428,7 @@ function listWorkflows() {
|
|
|
428
428
|
const builtinTag = BUILTIN_WORKFLOWS.some((b) => b.id === w.id)
|
|
429
429
|
? chalk.gray(" (built-in)")
|
|
430
430
|
: "";
|
|
431
|
-
console.log(` ${chalk.bold(w.id)}${builtinTag}
|
|
431
|
+
console.log(` ${chalk.bold(w.id)}${builtinTag} ${statusStr}`);
|
|
432
432
|
console.log(chalk.gray(` ${w.description}`));
|
|
433
433
|
console.log(chalk.gray(` Steps: ${w.steps.map((s) => s.id).join(" → ")}`));
|
|
434
434
|
console.log();
|
|
@@ -464,7 +464,7 @@ function showWorkflow(id) {
|
|
|
464
464
|
}
|
|
465
465
|
else if (stepState?.status === "failed") {
|
|
466
466
|
icon = chalk.red("FAIL");
|
|
467
|
-
statusLabel = chalk.red(`
|
|
467
|
+
statusLabel = chalk.red(` ${stepState.error?.slice(0, 60)}`);
|
|
468
468
|
}
|
|
469
469
|
const depsStr = step.needs.length > 0
|
|
470
470
|
? chalk.gray(` [needs: ${step.needs.join(", ")}]`)
|
|
@@ -481,7 +481,7 @@ function showStatus() {
|
|
|
481
481
|
const workflows = loadWorkflows();
|
|
482
482
|
const projectName = process.cwd().split("/").pop() || "project";
|
|
483
483
|
console.log();
|
|
484
|
-
console.log(chalk.bold.cyan(` Workflow Status
|
|
484
|
+
console.log(chalk.bold.cyan(` Workflow Status: ${projectName}`));
|
|
485
485
|
console.log();
|
|
486
486
|
let anyActive = false;
|
|
487
487
|
for (const w of workflows) {
|
|
@@ -499,7 +499,7 @@ function showStatus() {
|
|
|
499
499
|
statusIcon = chalk.green("done");
|
|
500
500
|
if (state.status === "failed")
|
|
501
501
|
statusIcon = chalk.red("failed");
|
|
502
|
-
console.log(` ${chalk.bold(w.id)} [${bar}] ${pct}%
|
|
502
|
+
console.log(` ${chalk.bold(w.id)} [${bar}] ${pct}% ${statusIcon}`);
|
|
503
503
|
console.log(chalk.gray(` ${completed}/${total} steps complete`));
|
|
504
504
|
console.log();
|
|
505
505
|
}
|
|
@@ -583,7 +583,7 @@ export async function handleWorkflow(options) {
|
|
|
583
583
|
}
|
|
584
584
|
const projectName = process.cwd().split("/").pop() || "project";
|
|
585
585
|
console.log();
|
|
586
|
-
console.log(chalk.bold.cyan(` ArchByte Workflow: ${workflow.name}
|
|
586
|
+
console.log(chalk.bold.cyan(` ArchByte Workflow: ${workflow.name} | ${projectName}`));
|
|
587
587
|
console.log(chalk.gray(` ${workflow.description}`));
|
|
588
588
|
console.log(chalk.gray(` Steps: ${workflow.steps.length}`));
|
|
589
589
|
await runWorkflow(workflow);
|
package/dist/cli/yaml-io.js
CHANGED
|
@@ -36,7 +36,7 @@ export function writeSpec(rootDir, spec) {
|
|
|
36
36
|
if (!fs.existsSync(dir)) {
|
|
37
37
|
fs.mkdirSync(dir, { recursive: true });
|
|
38
38
|
}
|
|
39
|
-
const header = `# Generated by ArchByte
|
|
39
|
+
const header = `# Generated by ArchByte ${new Date().toISOString().split("T")[0]}\n# Human-editable. Changes are preserved on re-scan.\n\n`;
|
|
40
40
|
const yamlStr = yaml.dump(spec, {
|
|
41
41
|
indent: 2,
|
|
42
42
|
lineWidth: 120,
|
package/dist/server/src/index.js
CHANGED
|
@@ -1384,4 +1384,16 @@ export async function startServer(cfg) {
|
|
|
1384
1384
|
setupWatcher();
|
|
1385
1385
|
console.error(`[archbyte] Serving ${config.name}`);
|
|
1386
1386
|
console.error(`[archbyte] Diagram: ${config.diagramPath}`);
|
|
1387
|
+
// Listen for 'q' keypress to quit gracefully
|
|
1388
|
+
if (process.stdin.isTTY) {
|
|
1389
|
+
process.stdin.setRawMode(true);
|
|
1390
|
+
process.stdin.resume();
|
|
1391
|
+
process.stdin.setEncoding("utf8");
|
|
1392
|
+
process.stdin.on("data", (key) => {
|
|
1393
|
+
if (key === "q" || key === "Q" || key === "\x03") {
|
|
1394
|
+
process.emit("SIGINT");
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
console.error(`[archbyte] Press q to quit`);
|
|
1398
|
+
}
|
|
1387
1399
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archbyte",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "ArchByte - See what agents build. As they build it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,11 +37,13 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@anthropic-ai/sdk": "^0.74.0",
|
|
39
39
|
"@google/generative-ai": "^0.24.1",
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
40
41
|
"chalk": "^5.3.0",
|
|
41
42
|
"chokidar": "^3.5.3",
|
|
42
43
|
"commander": "^12.0.0",
|
|
43
44
|
"js-yaml": "^4.1.1",
|
|
44
|
-
"openai": "^6.19.0"
|
|
45
|
+
"openai": "^6.19.0",
|
|
46
|
+
"zod": "^4.3.6"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
49
|
"@types/js-yaml": "^4.0.9",
|
|
Binary file
|
|
Binary file
|
package/ui/dist/index.html
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>ArchByte</title>
|
|
7
|
-
<link rel="icon" href="/favicon.
|
|
7
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
|
|
8
|
+
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png">
|
|
8
9
|
<script type="module" crossorigin src="/assets/index-BdfGbhpp.js"></script>
|
|
9
10
|
<link rel="stylesheet" crossorigin href="/assets/index-0_XpUUZQ.css">
|
|
10
11
|
</head>
|