archbyte 0.3.5 → 0.4.1

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.
Files changed (45) hide show
  1. package/README.md +42 -0
  2. package/bin/archbyte.js +26 -25
  3. package/dist/agents/pipeline/merger.d.ts +2 -2
  4. package/dist/agents/pipeline/merger.js +165 -35
  5. package/dist/agents/pipeline/types.d.ts +29 -1
  6. package/dist/agents/pipeline/types.js +0 -1
  7. package/dist/agents/providers/claude-sdk.d.ts +7 -0
  8. package/dist/agents/providers/claude-sdk.js +83 -0
  9. package/dist/agents/providers/router.d.ts +5 -0
  10. package/dist/agents/providers/router.js +23 -1
  11. package/dist/agents/runtime/types.d.ts +6 -2
  12. package/dist/agents/runtime/types.js +6 -1
  13. package/dist/agents/static/component-detector.js +35 -3
  14. package/dist/agents/static/connection-mapper.d.ts +1 -1
  15. package/dist/agents/static/connection-mapper.js +74 -1
  16. package/dist/agents/static/index.js +5 -2
  17. package/dist/agents/static/types.d.ts +26 -0
  18. package/dist/cli/analyze.js +65 -18
  19. package/dist/cli/arch-diff.d.ts +38 -0
  20. package/dist/cli/arch-diff.js +61 -0
  21. package/dist/cli/auth.d.ts +8 -2
  22. package/dist/cli/auth.js +241 -31
  23. package/dist/cli/config.js +31 -5
  24. package/dist/cli/export.js +64 -2
  25. package/dist/cli/patrol.d.ts +5 -3
  26. package/dist/cli/patrol.js +417 -65
  27. package/dist/cli/setup.js +76 -8
  28. package/dist/cli/shared.d.ts +11 -0
  29. package/dist/cli/shared.js +61 -0
  30. package/dist/cli/ui.d.ts +9 -0
  31. package/dist/cli/ui.js +59 -5
  32. package/dist/cli/validate.d.ts +0 -1
  33. package/dist/cli/validate.js +0 -16
  34. package/dist/server/src/index.js +593 -19
  35. package/package.json +4 -1
  36. package/templates/archbyte.yaml +8 -0
  37. package/ui/dist/assets/index-DDCNauh7.css +1 -0
  38. package/ui/dist/assets/index-DO4t5Xu1.js +72 -0
  39. package/ui/dist/index.html +2 -2
  40. package/dist/cli/mcp-server.d.ts +0 -1
  41. package/dist/cli/mcp-server.js +0 -443
  42. package/dist/cli/mcp.d.ts +0 -1
  43. package/dist/cli/mcp.js +0 -98
  44. package/ui/dist/assets/index-0_XpUUZQ.css +0 -1
  45. package/ui/dist/assets/index-BTo0zV5E.js +0 -70
@@ -2,10 +2,15 @@
2
2
  // === Model Routing ===
3
3
  export const MODEL_MAP = {
4
4
  anthropic: {
5
- fast: "claude-haiku-4-5-20251001",
5
+ fast: "claude-sonnet-4-5-20250929",
6
6
  standard: "claude-sonnet-4-5-20250929",
7
7
  advanced: "claude-opus-4-6",
8
8
  },
9
+ "claude-sdk": {
10
+ fast: "sonnet",
11
+ standard: "sonnet",
12
+ advanced: "opus",
13
+ },
9
14
  openai: {
10
15
  fast: "gpt-5.2",
11
16
  standard: "gpt-5.2",
@@ -74,15 +74,21 @@ export async function detectComponents(tk, structure) {
74
74
  // Strategy 1: Monorepo workspaces
75
75
  if (structure.isMonorepo) {
76
76
  const components = await detectFromWorkspaces(tk, structure);
77
- if (components.length > 0)
77
+ if (components.length > 0) {
78
+ await enrichWithFileMetrics(tk, components);
78
79
  return { components };
80
+ }
79
81
  }
80
82
  // Strategy 2: Scan ALL top-level directories for build configs + conventional names
81
83
  const components = await detectAllComponents(tk, structure);
82
- if (components.length > 0)
84
+ if (components.length > 0) {
85
+ await enrichWithFileMetrics(tk, components);
83
86
  return { components };
87
+ }
84
88
  // Strategy 3: Single app fallback
85
- return { components: [buildSingleAppComponent(structure)] };
89
+ const fallback = [buildSingleAppComponent(structure)];
90
+ await enrichWithFileMetrics(tk, fallback);
91
+ return { components: fallback };
86
92
  }
87
93
  async function detectFromWorkspaces(tk, structure) {
88
94
  // Collect workspace patterns from package.json OR pnpm-workspace.yaml
@@ -383,6 +389,32 @@ function extractTechStack(pkg) {
383
389
  function capitalize(s) {
384
390
  return s.charAt(0).toUpperCase() + s.slice(1);
385
391
  }
392
+ const CODE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|go|rs|java|kt|rb|cs|php|swift|dart)$/i;
393
+ async function enrichWithFileMetrics(tk, components) {
394
+ await Promise.all(components.map(async (comp) => {
395
+ if (comp.path === ".")
396
+ return;
397
+ try {
398
+ const allFiles = await tk.globFiles(`**/*`, comp.path);
399
+ const codeFiles = allFiles.filter((f) => CODE_EXTENSIONS.test(f));
400
+ let totalLines = 0;
401
+ const filesToCount = codeFiles.slice(0, 150);
402
+ await Promise.all(filesToCount.map(async (file) => {
403
+ const content = await tk.readFileSafe(file);
404
+ if (content)
405
+ totalLines += content.split("\n").length;
406
+ }));
407
+ comp.fileMetrics = {
408
+ fileCount: allFiles.length,
409
+ totalLines,
410
+ codeFileCount: codeFiles.length,
411
+ };
412
+ }
413
+ catch {
414
+ // skip
415
+ }
416
+ }));
417
+ }
386
418
  function extractFirstParagraph(readme) {
387
419
  const lines = readme.split("\n");
388
420
  let capturing = false;
@@ -1,3 +1,3 @@
1
1
  import type { ConnectionResult, StaticComponent, InfraResult, EventResult } from "./types.js";
2
2
  import type { StaticToolkit } from "./utils.js";
3
- export declare function mapConnections(tk: StaticToolkit, components: StaticComponent[], infra: InfraResult, events: EventResult): Promise<ConnectionResult>;
3
+ export declare function mapConnections(tk: StaticToolkit, components: StaticComponent[], infra: InfraResult, events: EventResult, importMap?: Record<string, string[]>): Promise<ConnectionResult>;
@@ -2,7 +2,7 @@
2
2
  // Maps connections between components via imports, Docker, K8s, env vars, known SDKs
3
3
  // Only deterministic matches — ambiguous mappings are left as gaps for the LLM
4
4
  import { slugify } from "./utils.js";
5
- export async function mapConnections(tk, components, infra, events) {
5
+ export async function mapConnections(tk, components, infra, events, importMap) {
6
6
  const connections = [];
7
7
  const componentIds = new Set(components.map((c) => c.id));
8
8
  // Run all detection methods in parallel
@@ -10,6 +10,7 @@ export async function mapConnections(tk, components, infra, events) {
10
10
  detectDockerDependencies(infra, components, connections),
11
11
  detectK8sIngress(infra, components, connections),
12
12
  detectImportConnections(tk, components, connections),
13
+ detectImportMapConnections(components, connections, importMap ?? {}),
13
14
  detectDatabaseConnections(tk, components, connections),
14
15
  detectServerServesUI(tk, components, connections),
15
16
  detectKnownSDKConnections(components, connections),
@@ -225,6 +226,78 @@ async function detectKnownSDKConnections(components, connections) {
225
226
  }
226
227
  }
227
228
  }
229
+ /**
230
+ * Detect cross-component connections from the code-sampler import map.
231
+ * Resolves relative imports and workspace package names to component boundaries.
232
+ */
233
+ async function detectImportMapConnections(components, connections, importMap) {
234
+ if (Object.keys(importMap).length === 0)
235
+ return;
236
+ // Build component path → id lookup, sorted longest-first for greedy matching
237
+ const compPathEntries = components
238
+ .filter((c) => c.path !== ".")
239
+ .map((c) => ({ path: c.path.endsWith("/") ? c.path : c.path + "/", id: c.id }))
240
+ .sort((a, b) => b.path.length - a.path.length);
241
+ // Build package name → component id lookup (for monorepo imports)
242
+ const pkgNameToComp = new Map();
243
+ for (const comp of components) {
244
+ pkgNameToComp.set(comp.id, comp.id);
245
+ pkgNameToComp.set(comp.name, comp.id);
246
+ // Handle scoped package names: @org/name → name
247
+ const unscoped = comp.name.replace(/^@[^/]+\//, "");
248
+ if (unscoped !== comp.name)
249
+ pkgNameToComp.set(unscoped, comp.id);
250
+ }
251
+ const findCompForFile = (filePath) => {
252
+ for (const entry of compPathEntries) {
253
+ if (filePath.startsWith(entry.path) || filePath === entry.path.slice(0, -1)) {
254
+ return entry.id;
255
+ }
256
+ }
257
+ return null;
258
+ };
259
+ // Track seen connections to avoid duplicates within this strategy
260
+ const seen = new Set();
261
+ for (const [filePath, imports] of Object.entries(importMap)) {
262
+ const sourceComp = findCompForFile(filePath);
263
+ if (!sourceComp)
264
+ continue;
265
+ for (const imp of imports) {
266
+ let targetComp = null;
267
+ if (imp.startsWith(".") || imp.startsWith("/")) {
268
+ // Relative import — resolve path
269
+ const fromDir = filePath.substring(0, filePath.lastIndexOf("/"));
270
+ const parts = fromDir.split("/");
271
+ for (const seg of imp.split("/")) {
272
+ if (seg === "..")
273
+ parts.pop();
274
+ else if (seg !== ".")
275
+ parts.push(seg);
276
+ }
277
+ targetComp = findCompForFile(parts.join("/") + "/");
278
+ }
279
+ else {
280
+ // Package/bare import — check if it maps to a workspace component
281
+ const name = imp.split("/").slice(0, imp.startsWith("@") ? 2 : 1).join("/");
282
+ targetComp = pkgNameToComp.get(name) ?? pkgNameToComp.get(name.replace(/^@[^/]+\//, "")) ?? null;
283
+ }
284
+ if (targetComp && targetComp !== sourceComp) {
285
+ const key = `${sourceComp}::${targetComp}`;
286
+ if (seen.has(key))
287
+ continue;
288
+ seen.add(key);
289
+ connections.push({
290
+ from: sourceComp,
291
+ to: targetComp,
292
+ type: "import",
293
+ description: `Import: ${sourceComp} → ${targetComp}`,
294
+ confidence: 85,
295
+ async: false,
296
+ });
297
+ }
298
+ }
299
+ }
300
+ }
228
301
  /**
229
302
  * Exact match only: id, slug, path, or name must match exactly.
230
303
  * No fuzzy matching, synonyms, or partial matches — those are for the LLM.
@@ -37,9 +37,12 @@ export async function runStaticAnalysis(projectRoot, onProgress) {
37
37
  onProgress?.("Detecting components...");
38
38
  const components = await detectComponents(tk, structure);
39
39
  onProgress?.(`Found ${components.components.length} component(s)`);
40
- // Phase 3: connection mapping (needs components + infra + events)
40
+ // Phase 2.5: collect import map for enriched connection detection
41
+ onProgress?.("Building import map...");
42
+ const codeSamples = await collectCodeSamples(tk);
43
+ // Phase 3: connection mapping (needs components + infra + events + import map)
41
44
  onProgress?.("Mapping connections...");
42
- const connections = await mapConnections(tk, components.components, infra, events);
45
+ const connections = await mapConnections(tk, components.components, infra, events, codeSamples.importMap);
43
46
  onProgress?.(`Found ${connections.connections.length} connection(s)`);
44
47
  // Assemble result
45
48
  const analysis = {
@@ -32,6 +32,20 @@ export interface StaticComponent {
32
32
  path: string;
33
33
  description: string;
34
34
  technologies: string[];
35
+ /** Coupling: count of public exports (from enricher) */
36
+ interfaceSurface?: number;
37
+ /** Security: has auth/permission boundary (from enricher) */
38
+ hasBoundary?: boolean;
39
+ /** Security: type of boundary e.g. "auth-middleware", "rbac" */
40
+ boundaryType?: string;
41
+ /** Key public export names (from enricher) */
42
+ publicExports?: string[];
43
+ /** File metrics from static analysis */
44
+ fileMetrics?: {
45
+ fileCount: number;
46
+ totalLines: number;
47
+ codeFileCount: number;
48
+ };
35
49
  }
36
50
  export interface InfraResult {
37
51
  docker: {
@@ -90,6 +104,8 @@ export interface StaticConnection {
90
104
  description: string;
91
105
  confidence: number;
92
106
  async: boolean;
107
+ /** Coupling weight 1-10 (from enricher) */
108
+ weight?: number;
93
109
  }
94
110
  export interface ConnectionResult {
95
111
  connections: StaticConnection[];
@@ -130,6 +146,16 @@ export interface StaticAnalysisResult {
130
146
  validation: ValidationResult;
131
147
  /** Gaps the static analysis couldn't resolve — passed to LLM for resolution */
132
148
  gaps: AnalysisGap[];
149
+ /** Free-form architectural observations from enricher */
150
+ enrichmentInsights?: string[];
151
+ /** Flow verification results from enricher */
152
+ flowVerifications?: Array<{
153
+ flowName: string;
154
+ verified: boolean;
155
+ stepsVerified: number;
156
+ stepsTotal: number;
157
+ issues?: string[];
158
+ }>;
133
159
  }
134
160
  export interface TreeEntry {
135
161
  path: string;
@@ -72,31 +72,38 @@ export async function handleAnalyze(options) {
72
72
  if (options.provider) {
73
73
  config = {
74
74
  provider: options.provider,
75
- apiKey: options.apiKey ?? config?.apiKey ?? "",
75
+ ...(options.provider !== "claude-sdk" ? { apiKey: options.apiKey ?? config?.apiKey ?? "" } : {}),
76
76
  };
77
77
  }
78
78
  if (options.apiKey && config) {
79
79
  config.apiKey = options.apiKey;
80
80
  }
81
81
  if (!config) {
82
- console.error(chalk.red("No model provider configured."));
83
- console.error();
84
- console.error(chalk.bold("Set up with:"));
85
- console.error(chalk.gray(" archbyte config set provider anthropic"));
86
- console.error(chalk.gray(" archbyte config set api-key sk-ant-..."));
87
- console.error();
88
- console.error(chalk.bold("Or use environment variables:"));
89
- console.error(chalk.gray(" export ARCHBYTE_PROVIDER=anthropic"));
90
- console.error(chalk.gray(" export ARCHBYTE_API_KEY=sk-ant-..."));
91
- console.error();
92
- console.error(chalk.bold("Or run without a model:"));
93
- console.error(chalk.gray(" archbyte analyze --static"));
94
- console.error();
95
- console.error(chalk.bold("Supported providers:"));
96
- console.error(chalk.gray(" anthropic, openai, google"));
97
- process.exit(1);
82
+ const msg = [
83
+ chalk.red("No model provider configured."),
84
+ "",
85
+ chalk.bold("Zero-config (Claude Code users):"),
86
+ chalk.gray(" Install Claude Code → archbyte analyze just works"),
87
+ "",
88
+ chalk.bold("Or set up with:"),
89
+ chalk.gray(" archbyte config set provider anthropic"),
90
+ chalk.gray(" archbyte config set api-key sk-ant-..."),
91
+ "",
92
+ chalk.bold("Or use environment variables:"),
93
+ chalk.gray(" export ARCHBYTE_PROVIDER=anthropic"),
94
+ chalk.gray(" export ARCHBYTE_API_KEY=sk-ant-..."),
95
+ "",
96
+ chalk.bold("Or run without a model:"),
97
+ chalk.gray(" archbyte analyze --static"),
98
+ "",
99
+ chalk.bold("Supported providers:"),
100
+ chalk.gray(" anthropic, openai, google, claude-sdk"),
101
+ ].join("\n");
102
+ console.error(msg);
103
+ throw new Error("No model provider configured");
98
104
  }
99
- console.log(chalk.gray(`Provider: ${chalk.white(config.provider)}`));
105
+ const providerLabel = config.provider === "claude-sdk" ? "Claude Code (SDK)" : config.provider;
106
+ console.log(chalk.gray(`Provider: ${chalk.white(providerLabel)}`));
100
107
  console.log(chalk.gray(`Project: ${chalk.white(path.basename(rootDir))}`));
101
108
  console.log();
102
109
  // 2. Create provider
@@ -173,6 +180,19 @@ export async function handleAnalyze(options) {
173
180
  console.log();
174
181
  }
175
182
  }
183
+ // 3b. File-tree drift detection (catches zero-commit and major project growth)
184
+ if (priorMeta && !options.force && !incrementalContext) {
185
+ const currentFileCount = countProjectFiles(rootDir);
186
+ const priorCount = priorMeta.filesScanned ?? 0;
187
+ if (currentFileCount > 0 && priorCount > 0 && currentFileCount <= priorCount * 1.5) {
188
+ // File count hasn't grown significantly — skip re-scan
189
+ console.log(chalk.green("No significant changes detected since last scan. Use --force to re-scan."));
190
+ return;
191
+ }
192
+ if (currentFileCount > priorCount) {
193
+ console.log(chalk.yellow(`File tree grew from ${priorCount} to ${currentFileCount} files — running full scan.`));
194
+ }
195
+ }
176
196
  // 4. Run static context collection → LLM pipeline
177
197
  const progress = progressBar(7);
178
198
  progress.update(0, "Collecting static context...");
@@ -324,6 +344,33 @@ function getGitCommit() {
324
344
  return undefined;
325
345
  }
326
346
  }
347
+ function countProjectFiles(rootDir) {
348
+ let count = 0;
349
+ const skip = new Set(["node_modules", ".git", ".archbyte", ".next", "dist", "build", ".cache"]);
350
+ function walk(dir, depth) {
351
+ if (depth > 4)
352
+ return;
353
+ try {
354
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
355
+ for (const entry of entries) {
356
+ if (entry.name.startsWith(".") && entry.name !== ".github")
357
+ continue;
358
+ if (skip.has(entry.name))
359
+ continue;
360
+ const full = path.join(dir, entry.name);
361
+ if (entry.isDirectory()) {
362
+ walk(full, depth + 1);
363
+ }
364
+ else if (/\.(ts|tsx|js|jsx|py|go|rs|java|kt|rb|cs|php|swift|dart)$/i.test(entry.name)) {
365
+ count++;
366
+ }
367
+ }
368
+ }
369
+ catch { /* permission errors, etc. */ }
370
+ }
371
+ walk(rootDir, 0);
372
+ return count;
373
+ }
327
374
  function writeScanMetadata(rootDir, durationMs, mode, filesScanned, tokenUsage, incrementalMode, skippedAgents) {
328
375
  const meta = {
329
376
  analyzedAt: new Date().toISOString(),
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Architecture diff — computes structural changes between two architecture snapshots.
3
+ * Used by patrol for change detection and by diff command for comparison.
4
+ */
5
+ import type { Architecture } from "../server/src/generator/index.js";
6
+ export interface StructuralDiff {
7
+ addedNodes: Array<{
8
+ id: string;
9
+ label: string;
10
+ type: string;
11
+ layer: string;
12
+ }>;
13
+ removedNodes: Array<{
14
+ id: string;
15
+ label: string;
16
+ type: string;
17
+ layer: string;
18
+ }>;
19
+ modifiedNodes: Array<{
20
+ id: string;
21
+ field: string;
22
+ from: string;
23
+ to: string;
24
+ }>;
25
+ addedEdges: Array<{
26
+ source: string;
27
+ target: string;
28
+ label?: string;
29
+ }>;
30
+ removedEdges: Array<{
31
+ source: string;
32
+ target: string;
33
+ label?: string;
34
+ }>;
35
+ }
36
+ export declare function diffArchitectures(prev: Architecture, curr: Architecture): StructuralDiff;
37
+ export declare function loadArchitectureJSON(rootDir: string): Architecture | null;
38
+ export declare function hasStructuralChanges(diff: StructuralDiff): boolean;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Architecture diff — computes structural changes between two architecture snapshots.
3
+ * Used by patrol for change detection and by diff command for comparison.
4
+ */
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ export function diffArchitectures(prev, curr) {
8
+ const prevNodeMap = new Map();
9
+ for (const n of prev.nodes)
10
+ prevNodeMap.set(n.id, n);
11
+ const currNodeMap = new Map();
12
+ for (const n of curr.nodes)
13
+ currNodeMap.set(n.id, n);
14
+ const prevNodeIds = new Set(prev.nodes.map((n) => n.id));
15
+ const currNodeIds = new Set(curr.nodes.map((n) => n.id));
16
+ const addedNodes = curr.nodes
17
+ .filter((n) => !prevNodeIds.has(n.id))
18
+ .map((n) => ({ id: n.id, label: n.label, type: n.type, layer: n.layer }));
19
+ const removedNodes = prev.nodes
20
+ .filter((n) => !currNodeIds.has(n.id))
21
+ .map((n) => ({ id: n.id, label: n.label, type: n.type, layer: n.layer }));
22
+ const modifiedNodes = [];
23
+ for (const n of curr.nodes) {
24
+ const old = prevNodeMap.get(n.id);
25
+ if (!old)
26
+ continue;
27
+ for (const field of ["label", "type", "layer"]) {
28
+ if (old[field] !== n[field]) {
29
+ modifiedNodes.push({ id: n.id, field, from: old[field], to: n[field] });
30
+ }
31
+ }
32
+ }
33
+ const edgeKey = (e) => `${e.source}->${e.target}`;
34
+ const prevEdgeKeys = new Set(prev.edges.map(edgeKey));
35
+ const currEdgeKeys = new Set(curr.edges.map(edgeKey));
36
+ const addedEdges = curr.edges
37
+ .filter((e) => !prevEdgeKeys.has(edgeKey(e)))
38
+ .map((e) => ({ source: e.source, target: e.target, label: e.label }));
39
+ const removedEdges = prev.edges
40
+ .filter((e) => !currEdgeKeys.has(edgeKey(e)))
41
+ .map((e) => ({ source: e.source, target: e.target, label: e.label }));
42
+ return { addedNodes, removedNodes, modifiedNodes, addedEdges, removedEdges };
43
+ }
44
+ export function loadArchitectureJSON(rootDir) {
45
+ const archPath = path.join(rootDir, ".archbyte", "architecture.json");
46
+ if (!fs.existsSync(archPath))
47
+ return null;
48
+ try {
49
+ return JSON.parse(fs.readFileSync(archPath, "utf-8"));
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ export function hasStructuralChanges(diff) {
56
+ return (diff.addedNodes.length > 0 ||
57
+ diff.removedNodes.length > 0 ||
58
+ diff.modifiedNodes.length > 0 ||
59
+ diff.addedEdges.length > 0 ||
60
+ diff.removedEdges.length > 0);
61
+ }
@@ -5,10 +5,16 @@ interface Credentials {
5
5
  expiresAt: string;
6
6
  }
7
7
  export type OAuthProvider = "github" | "google";
8
- export declare function handleLogin(provider?: OAuthProvider): Promise<void>;
8
+ export type LoginProvider = OAuthProvider | "email";
9
+ export declare function handleLogin(provider?: LoginProvider): Promise<void>;
9
10
  export declare function handleLoginWithToken(token: string): Promise<void>;
10
- export declare function handleLogout(): Promise<void>;
11
+ export declare function handleLogout(options?: {
12
+ all?: boolean;
13
+ email?: string;
14
+ }): Promise<void>;
11
15
  export declare function handleStatus(): Promise<void>;
16
+ export declare function handleAccounts(): Promise<void>;
17
+ export declare function handleAccountSwitch(email?: string): Promise<void>;
12
18
  export declare function loadCredentials(): Credentials | null;
13
19
  /**
14
20
  * Get the tier from the JWT token payload. NEVER falls back to the