archbyte 0.2.2 → 0.2.5

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 (48) hide show
  1. package/README.md +33 -5
  2. package/bin/archbyte.js +47 -7
  3. package/dist/agents/prompt-data.js +4 -4
  4. package/dist/agents/providers/router.js +1 -16
  5. package/dist/agents/runtime/types.d.ts +5 -1
  6. package/dist/agents/runtime/types.js +3 -3
  7. package/dist/agents/static/code-sampler.js +1 -4
  8. package/dist/agents/static/component-detector.js +1 -4
  9. package/dist/agents/static/event-detector.js +2 -1
  10. package/dist/agents/static/excluded-dirs.d.ts +3 -0
  11. package/dist/agents/static/excluded-dirs.js +11 -0
  12. package/dist/agents/static/file-tree-collector.js +1 -5
  13. package/dist/agents/tools/claude-code.js +2 -5
  14. package/dist/agents/tools/local-fs.js +2 -6
  15. package/dist/cli/analyze.d.ts +1 -0
  16. package/dist/cli/analyze.js +5 -5
  17. package/dist/cli/auth.js +8 -6
  18. package/dist/cli/config.d.ts +1 -0
  19. package/dist/cli/config.js +80 -61
  20. package/dist/cli/constants.d.ts +13 -0
  21. package/dist/cli/constants.js +20 -0
  22. package/dist/cli/diff.js +1 -1
  23. package/dist/cli/export.js +3 -3
  24. package/dist/cli/gate.js +3 -3
  25. package/dist/cli/generate.js +2 -1
  26. package/dist/cli/license-gate.js +5 -5
  27. package/dist/cli/mcp-server.d.ts +1 -0
  28. package/dist/cli/mcp-server.js +443 -0
  29. package/dist/cli/mcp.d.ts +1 -0
  30. package/dist/cli/mcp.js +102 -0
  31. package/dist/cli/patrol.js +4 -4
  32. package/dist/cli/run.d.ts +1 -0
  33. package/dist/cli/run.js +3 -7
  34. package/dist/cli/serve.js +3 -2
  35. package/dist/cli/setup.js +153 -72
  36. package/dist/cli/stats.js +1 -1
  37. package/dist/cli/ui.js +3 -3
  38. package/dist/cli/validate.js +2 -2
  39. package/dist/cli/version.d.ts +2 -0
  40. package/dist/cli/version.js +84 -0
  41. package/dist/cli/workflow.js +7 -7
  42. package/dist/cli/yaml-io.js +1 -1
  43. package/dist/server/src/index.js +52 -0
  44. package/package.json +4 -2
  45. package/ui/dist/assets/{index-Bl1r8zrI.css → index-0_XpUUZQ.css} +1 -1
  46. package/ui/dist/assets/index-BdfGbhpp.js +70 -0
  47. package/ui/dist/index.html +2 -2
  48. package/ui/dist/assets/index-CqbB6DOK.js +0 -70
package/dist/cli/setup.js CHANGED
@@ -1,20 +1,38 @@
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";
7
8
  import { select, spinner, confirm } from "./ui.js";
9
+ import { CONFIG_DIR, CONFIG_PATH } from "./constants.js";
8
10
  const __filename = fileURLToPath(import.meta.url);
9
11
  const __dirname = path.dirname(__filename);
10
- const CONFIG_DIR = path.join(process.env.HOME ?? process.env.USERPROFILE ?? ".", ".archbyte");
11
- const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
12
12
  const PROVIDERS = [
13
- { name: "anthropic", label: "Anthropic", hint: "Claude Opus / Sonnet" },
14
- { name: "openai", label: "OpenAI", hint: "GPT-4o / o1" },
13
+ { name: "anthropic", label: "Anthropic", hint: "Claude Sonnet / Opus" },
14
+ { name: "openai", label: "OpenAI", hint: "Codex / GPT-5.2" },
15
15
  { name: "google", label: "Google", hint: "Gemini Flash / Pro" },
16
- { name: "ollama", label: "Ollama", hint: "Local models (no key needed)" },
17
16
  ];
17
+ const PROVIDER_MODELS = {
18
+ anthropic: [
19
+ { id: "", label: "Default (recommended)", hint: "Haiku for fast agents, Sonnet for connections" },
20
+ { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5", hint: "Fastest, cheapest" },
21
+ { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", hint: "Balanced, great quality" },
22
+ { id: "claude-opus-4-6", label: "Claude Opus 4.6", hint: "Most capable" },
23
+ ],
24
+ openai: [
25
+ { id: "", label: "Default (recommended)", hint: "Codex for fast agents, GPT-5.2 for connections" },
26
+ { id: "gpt-5.2-codex", label: "GPT-5.2 Codex", hint: "Fast, coding-optimized" },
27
+ { id: "gpt-5.2", label: "GPT-5.2", hint: "General purpose, thinking model" },
28
+ { id: "o3", label: "o3", hint: "Reasoning model" },
29
+ ],
30
+ google: [
31
+ { id: "", label: "Default (recommended)", hint: "Flash for fast agents, Pro for connections" },
32
+ { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash", hint: "Fastest, cheapest" },
33
+ { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro", hint: "Most capable" },
34
+ ],
35
+ };
18
36
  function loadConfig() {
19
37
  try {
20
38
  if (fs.existsSync(CONFIG_PATH)) {
@@ -75,40 +93,8 @@ function askHidden(prompt) {
75
93
  stdin.on("data", onData);
76
94
  });
77
95
  }
78
- async function fetchOllamaModels(baseUrl) {
96
+ async function validateProviderSilent(providerName, apiKey) {
79
97
  try {
80
- const controller = new AbortController();
81
- const timeout = setTimeout(() => controller.abort(), 5000);
82
- const res = await fetch(`${baseUrl}/api/tags`, { signal: controller.signal });
83
- clearTimeout(timeout);
84
- if (!res.ok)
85
- return [];
86
- const data = await res.json();
87
- return (data.models ?? []).map((m) => m.name);
88
- }
89
- catch {
90
- return [];
91
- }
92
- }
93
- async function validateProviderSilent(providerName, apiKey, ollamaBaseUrl) {
94
- try {
95
- if (providerName === "ollama") {
96
- const url = ollamaBaseUrl ?? "http://localhost:11434";
97
- const controller = new AbortController();
98
- const timeout = setTimeout(() => controller.abort(), 5000);
99
- try {
100
- const res = await fetch(url, { signal: controller.signal });
101
- clearTimeout(timeout);
102
- if (!res.ok)
103
- return false;
104
- // Model listing happens in setup flow, not here
105
- return true;
106
- }
107
- catch {
108
- clearTimeout(timeout);
109
- return false;
110
- }
111
- }
112
98
  const provider = createProvider({ provider: providerName, apiKey });
113
99
  const model = resolveModel(providerName, "fast");
114
100
  await provider.chat({
@@ -123,61 +109,151 @@ async function validateProviderSilent(providerName, apiKey, ollamaBaseUrl) {
123
109
  return false;
124
110
  }
125
111
  }
112
+ function getProfiles(config) {
113
+ return config.profiles ?? {};
114
+ }
115
+ function isInPath(cmd) {
116
+ try {
117
+ execSync(`which ${cmd}`, { stdio: "ignore" });
118
+ return true;
119
+ }
120
+ catch {
121
+ return false;
122
+ }
123
+ }
126
124
  export async function handleSetup() {
127
125
  console.log();
128
126
  console.log(chalk.bold.cyan("ArchByte Setup"));
129
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
+ }
130
157
  const config = loadConfig();
131
- // Show current config if exists
132
- if (config.provider) {
133
- console.log(chalk.gray(` Current provider: ${config.provider}`));
134
- if (config.apiKey) {
135
- console.log(chalk.gray(` Current API key: ${maskKey(config.apiKey)}`));
158
+ const profiles = getProfiles(config);
159
+ // Migrate legacy flat config → profiles
160
+ if (!config.profiles && config.provider && config.apiKey) {
161
+ const legacy = config.provider;
162
+ const validNames = PROVIDERS.map((p) => p.name);
163
+ if (validNames.includes(legacy)) {
164
+ // Known provider — migrate to profile
165
+ profiles[legacy] = { apiKey: config.apiKey };
166
+ if (config.model)
167
+ profiles[legacy].model = config.model;
168
+ config.profiles = profiles;
169
+ }
170
+ // Clean up legacy flat keys regardless
171
+ delete config.apiKey;
172
+ delete config.model;
173
+ delete config.ollamaBaseUrl;
174
+ if (!validNames.includes(legacy)) {
175
+ delete config.provider;
176
+ }
177
+ }
178
+ // Show current config + configured profiles
179
+ const configured = Object.keys(profiles).filter((p) => profiles[p]?.apiKey);
180
+ if (configured.length > 0) {
181
+ console.log(chalk.gray(` Active provider: ${config.provider ?? "none"}`));
182
+ for (const p of configured) {
183
+ const active = p === config.provider ? chalk.green(" (active)") : "";
184
+ const model = profiles[p].model ? chalk.gray(` model: ${profiles[p].model}`) : "";
185
+ console.log(chalk.gray(` ${p}: ${maskKey(profiles[p].apiKey)}${model}${active}`));
136
186
  }
137
187
  console.log();
138
188
  }
139
- // Step 1: Choose provider with arrow-key selection
189
+ // Step 1: Choose provider
140
190
  const idx = await select("Choose your model provider:", PROVIDERS.map((p) => {
141
- const current = config.provider === p.name ? chalk.green(" (current)") : "";
142
- return `${p.label} ${chalk.gray(" " + p.hint)}${current}`;
191
+ const active = config.provider === p.name ? chalk.green(" (active)") : "";
192
+ const hasKey = profiles[p.name]?.apiKey ? chalk.cyan(" ✓ configured") : "";
193
+ return `${p.label} ${chalk.gray(p.hint)}${hasKey}${active}`;
143
194
  }));
144
195
  const provider = PROVIDERS[idx].name;
145
196
  config.provider = provider;
146
197
  const selected = PROVIDERS[idx];
147
198
  console.log(chalk.green(`\n ✓ Provider: ${selected.label}`));
148
- // Step 2: API key / model selection
149
- if (provider === "ollama") {
150
- config.ollamaBaseUrl = config.ollamaBaseUrl ?? "http://localhost:11434";
151
- console.log(chalk.gray(` Ollama URL: ${config.ollamaBaseUrl}`));
152
- // Let user pick from installed models
153
- const models = await fetchOllamaModels(config.ollamaBaseUrl);
154
- if (models.length > 0) {
155
- const modelIdx = await select("\n Choose a model:", models.map((m) => m));
156
- config.model = models[modelIdx];
157
- console.log(chalk.green(` ✓ Model: ${config.model}`));
199
+ // Step 2: API key (show existing if profile exists)
200
+ const existing = profiles[provider];
201
+ let apiKey;
202
+ if (existing?.apiKey) {
203
+ console.log(chalk.gray(`\n Existing key: ${maskKey(existing.apiKey)}`));
204
+ const keepIdx = await select(" Update API key?", [
205
+ `Keep existing ${chalk.gray("(" + maskKey(existing.apiKey) + ")")}`,
206
+ "Enter new key",
207
+ ]);
208
+ if (keepIdx === 0) {
209
+ apiKey = existing.apiKey;
210
+ console.log(chalk.green(` ✓ API key: ${maskKey(apiKey)} (kept)`));
158
211
  }
159
212
  else {
160
- console.log(chalk.yellow(` Warning: no models installed.`));
161
- console.log(chalk.gray(` Run: ollama pull <model-name>`));
213
+ console.log();
214
+ apiKey = await askHidden(chalk.bold(" API key: "));
215
+ if (!apiKey) {
216
+ console.log(chalk.red(" No API key entered. Setup cancelled."));
217
+ process.exit(1);
218
+ }
219
+ console.log(chalk.green(` ✓ API key: ${maskKey(apiKey)}`));
162
220
  }
163
221
  }
164
222
  else {
165
- // Clear Ollama model override — model names are provider-specific
166
- if (config.model) {
167
- delete config.model;
168
- }
169
223
  console.log();
170
- const apiKey = await askHidden(chalk.bold(" API key: "));
224
+ apiKey = await askHidden(chalk.bold(" API key: "));
171
225
  if (!apiKey) {
172
226
  console.log(chalk.red(" No API key entered. Setup cancelled."));
173
227
  process.exit(1);
174
228
  }
175
- config.apiKey = apiKey;
176
229
  console.log(chalk.green(` ✓ API key: ${maskKey(apiKey)}`));
177
230
  }
231
+ // Save to profile
232
+ if (!profiles[provider])
233
+ profiles[provider] = { apiKey: "" };
234
+ profiles[provider].apiKey = apiKey;
235
+ // Step 3: Model selection
236
+ const models = PROVIDER_MODELS[provider];
237
+ if (models) {
238
+ const currentModel = profiles[provider].model;
239
+ const modelIdx = await select("\n Choose a model:", models.map((m) => {
240
+ const current = currentModel === m.id && m.id ? chalk.green(" (current)") : "";
241
+ return `${m.label} ${chalk.gray(m.hint)}${current}`;
242
+ }));
243
+ const chosen = models[modelIdx];
244
+ if (chosen.id) {
245
+ profiles[provider].model = chosen.id;
246
+ console.log(chalk.green(` ✓ Model: ${chosen.label}`));
247
+ }
248
+ else {
249
+ delete profiles[provider].model;
250
+ console.log(chalk.green(` ✓ Model: Default (auto per agent tier)`));
251
+ }
252
+ }
253
+ config.profiles = profiles;
178
254
  // Validate provider with spinner
179
255
  const s = spinner("Validating credentials");
180
- let isValid = await validateProviderSilent(provider, config.apiKey ?? "", config.ollamaBaseUrl);
256
+ let isValid = await validateProviderSilent(provider, apiKey);
181
257
  s.stop(isValid ? "valid" : "invalid", isValid ? "green" : "red");
182
258
  // Retry loop on failure
183
259
  if (!isValid) {
@@ -186,24 +262,28 @@ export async function handleSetup() {
186
262
  if (!await confirm(" Retry?"))
187
263
  break;
188
264
  retries++;
189
- if (provider !== "ollama") {
190
- const newKey = await askHidden(chalk.bold(" API key: "));
191
- if (newKey) {
192
- config.apiKey = newKey;
193
- console.log(chalk.green(` ✓ API key: ${maskKey(newKey)}`));
194
- }
265
+ const newKey = await askHidden(chalk.bold(" API key: "));
266
+ if (newKey) {
267
+ profiles[provider].apiKey = newKey;
268
+ console.log(chalk.green(` ✓ API key: ${maskKey(newKey)}`));
195
269
  }
196
270
  const s2 = spinner("Validating credentials");
197
- isValid = await validateProviderSilent(provider, config.apiKey ?? "", config.ollamaBaseUrl);
271
+ isValid = await validateProviderSilent(provider, profiles[provider].apiKey);
198
272
  s2.stop(isValid ? "valid" : "invalid", isValid ? "green" : "red");
199
273
  }
200
274
  if (!isValid) {
201
275
  console.log(chalk.yellow(" Warning: credentials unverified. Saving anyway."));
202
276
  }
203
277
  }
278
+ // Clean up legacy top-level keys
279
+ delete config.apiKey;
280
+ delete config.model;
281
+ delete config.ollamaBaseUrl;
204
282
  // Save config
205
283
  saveConfig(config);
206
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."));
207
287
  // Generate archbyte.yaml in .archbyte/ if it doesn't exist
208
288
  const projectDir = process.cwd();
209
289
  const archbyteDir = path.join(projectDir, ".archbyte");
@@ -236,5 +316,6 @@ export async function handleSetup() {
236
316
  console.log(chalk.green(" Setup complete!"));
237
317
  console.log();
238
318
  console.log(chalk.bold(" Next: ") + chalk.cyan("archbyte run") + chalk.bold(" to analyze your codebase."));
319
+ console.log(chalk.gray(" Run from your project root, or use ") + chalk.cyan("archbyte run -d /path/to/project"));
239
320
  console.log();
240
321
  }
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 ${projectName}`));
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
  }
@@ -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 ${projectName}`));
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" : ""} ${resultStr}`);
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) {
@@ -0,0 +1,2 @@
1
+ export declare function handleVersion(): Promise<void>;
2
+ export declare function handleUpdate(): Promise<void>;
@@ -0,0 +1,84 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import chalk from "chalk";
5
+ import { execSync } from "child_process";
6
+ import { NETWORK_TIMEOUT_MS } from "./constants.js";
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ function getLocalVersion() {
10
+ const pkgPath = path.resolve(__dirname, "../../package.json");
11
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
12
+ return pkg.version;
13
+ }
14
+ export async function handleVersion() {
15
+ const version = getLocalVersion();
16
+ console.log();
17
+ console.log(chalk.bold.cyan("ArchByte") + chalk.gray(` v${version}`));
18
+ console.log();
19
+ console.log(` ${chalk.bold("version")}: ${version}`);
20
+ console.log(` ${chalk.bold("node")}: ${process.versions.node}`);
21
+ console.log(` ${chalk.bold("platform")}: ${process.platform} ${process.arch}`);
22
+ // Show install path
23
+ try {
24
+ const which = execSync("which archbyte", { encoding: "utf-8" }).trim();
25
+ console.log(` ${chalk.bold("binary")}: ${which}`);
26
+ }
27
+ catch {
28
+ // not globally installed or `which` not available
29
+ }
30
+ console.log();
31
+ }
32
+ async function fetchLatestVersion() {
33
+ try {
34
+ const controller = new AbortController();
35
+ const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS);
36
+ const res = await fetch("https://registry.npmjs.org/archbyte/latest", {
37
+ signal: controller.signal,
38
+ });
39
+ clearTimeout(timeout);
40
+ if (!res.ok)
41
+ return null;
42
+ const data = (await res.json());
43
+ return data.version ?? null;
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ export async function handleUpdate() {
50
+ const current = getLocalVersion();
51
+ console.log();
52
+ console.log(chalk.bold.cyan("ArchByte Update"));
53
+ console.log(chalk.gray(` Current version: ${current}`));
54
+ console.log(chalk.gray(" Checking for updates..."));
55
+ const latest = await fetchLatestVersion();
56
+ if (!latest) {
57
+ console.log(chalk.yellow(" Could not reach npm registry. Update manually:"));
58
+ console.log(chalk.cyan(" npm install -g archbyte@latest"));
59
+ console.log();
60
+ return;
61
+ }
62
+ if (latest === current) {
63
+ console.log(chalk.green(` You're on the latest version (${current}).`));
64
+ console.log();
65
+ return;
66
+ }
67
+ console.log(chalk.yellow(` New version available: ${current} → ${chalk.bold(latest)}`));
68
+ console.log();
69
+ // Run the update
70
+ console.log(chalk.gray(" Updating..."));
71
+ try {
72
+ execSync("npm install -g archbyte@latest", {
73
+ stdio: "inherit",
74
+ });
75
+ console.log();
76
+ console.log(chalk.green(` Updated to v${latest}`));
77
+ }
78
+ catch {
79
+ console.log();
80
+ console.log(chalk.red(" Update failed. Try manually:"));
81
+ console.log(chalk.cyan(" npm install -g archbyte@latest"));
82
+ }
83
+ console.log();
84
+ }
@@ -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} waiting on: ${unmetDeps.join(", ")}`));
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 ${projectName}`));
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} ${statusStr}`);
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(` ${stepState.error?.slice(0, 60)}`);
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 ${projectName}`));
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}% ${statusIcon}`);
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} ${projectName}`));
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);
@@ -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 ${new Date().toISOString().split("T")[0]}\n# Human-editable. Changes are preserved on re-scan.\n\n`;
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,
@@ -135,6 +135,46 @@ function createHttpServer() {
135
135
  res.end(JSON.stringify(currentArchitecture || { nodes: [], edges: [] }));
136
136
  return;
137
137
  }
138
+ // API: Update node positions (save layout changes)
139
+ if (url === "/api/update-positions" && req.method === "POST") {
140
+ let body = "";
141
+ req.on("data", (chunk) => { body += chunk.toString(); });
142
+ req.on("end", async () => {
143
+ try {
144
+ const { updates } = JSON.parse(body);
145
+ if (!updates || !Array.isArray(updates)) {
146
+ res.writeHead(400, { "Content-Type": "application/json" });
147
+ res.end(JSON.stringify({ error: "Missing updates array" }));
148
+ return;
149
+ }
150
+ // Read current architecture file
151
+ const content = await readFile(config.diagramPath, "utf-8");
152
+ const arch = JSON.parse(content);
153
+ // Apply position/dimension updates
154
+ for (const update of updates) {
155
+ const node = arch.nodes.find((n) => n.id === update.id);
156
+ if (node) {
157
+ node.x = update.x;
158
+ node.y = update.y;
159
+ node.width = update.width;
160
+ node.height = update.height;
161
+ }
162
+ }
163
+ // Mark layout as user-saved so UI uses these positions
164
+ arch.layoutSaved = true;
165
+ // Write back
166
+ await writeFile(config.diagramPath, JSON.stringify(arch, null, 2), "utf-8");
167
+ currentArchitecture = arch;
168
+ res.writeHead(200, { "Content-Type": "application/json" });
169
+ res.end(JSON.stringify({ ok: true, updated: updates.length }));
170
+ }
171
+ catch (err) {
172
+ res.writeHead(500, { "Content-Type": "application/json" });
173
+ res.end(JSON.stringify({ error: "Failed to save positions" }));
174
+ }
175
+ });
176
+ return;
177
+ }
138
178
  // API: Health
139
179
  if (url === "/health") {
140
180
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -1344,4 +1384,16 @@ export async function startServer(cfg) {
1344
1384
  setupWatcher();
1345
1385
  console.error(`[archbyte] Serving ${config.name}`);
1346
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
+ }
1347
1399
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archbyte",
3
- "version": "0.2.2",
3
+ "version": "0.2.5",
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",