archbyte 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ // Shared constants for the ArchByte CLI.
2
+ // Single source of truth for URLs, ports, paths, and timeouts.
3
+ import * as path from "path";
4
+ // ─── API ───
5
+ export const API_BASE = process.env.ARCHBYTE_API_URL ?? "https://api.heartbyte.io";
6
+ export const SITE_URL = "https://archbyte.heartbyte.io";
7
+ // ─── Ports ───
8
+ export const DEFAULT_PORT = 3847;
9
+ export const CLI_CALLBACK_PORT = 19274;
10
+ // ─── Paths ───
11
+ export const CONFIG_DIR = path.join(process.env.HOME ?? process.env.USERPROFILE ?? ".", ".archbyte");
12
+ export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
13
+ export const CREDENTIALS_PATH = path.join(CONFIG_DIR, "credentials.json");
14
+ /** Project-local .archbyte directory name */
15
+ export const PROJECT_DIR = ".archbyte";
16
+ // ─── Timeouts ───
17
+ /** Timeout for non-critical network checks (license, version, gate) */
18
+ export const NETWORK_TIMEOUT_MS = 5000;
19
+ /** Timeout for OAuth callback server */
20
+ export const OAUTH_TIMEOUT_MS = 60_000;
package/dist/cli/gate.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { loadCredentials, cacheVerifiedTier, resetOfflineActions, checkOfflineAction } from "./auth.js";
2
- const API_BASE = process.env.ARCHBYTE_API_URL ?? "https://api.heartbyte.io";
2
+ import { API_BASE, NETWORK_TIMEOUT_MS } from "./constants.js";
3
3
  /**
4
4
  * Auth gate command for Claude Code integration.
5
5
  *
@@ -48,7 +48,7 @@ async function checkGate(action) {
48
48
  "Content-Type": "application/json",
49
49
  },
50
50
  body: JSON.stringify({ action }),
51
- signal: AbortSignal.timeout(5000),
51
+ signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
52
52
  });
53
53
  if (res.status === 401) {
54
54
  const result = {
@@ -120,7 +120,7 @@ async function recordScan() {
120
120
  "Content-Type": "application/json",
121
121
  },
122
122
  body: JSON.stringify({}),
123
- signal: AbortSignal.timeout(5000),
123
+ signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
124
124
  });
125
125
  console.log(JSON.stringify({ recorded: true }));
126
126
  }
@@ -4,6 +4,7 @@ import chalk from "chalk";
4
4
  import { generateArchitecture } from "../server/src/generator/index.js";
5
5
  import { loadSpec, specToAnalysis } from "./yaml-io.js";
6
6
  import { spinner } from "./ui.js";
7
+ import { DEFAULT_PORT } from "./constants.js";
7
8
  /**
8
9
  * Generate excalidraw diagram from analysis JSON
9
10
  */
@@ -233,7 +234,7 @@ export async function handleGenerate(options) {
233
234
  console.log();
234
235
  console.log(chalk.bold("Next steps:"));
235
236
  console.log(chalk.gray(` 1. Run ${chalk.cyan("archbyte serve")} to start the visualization server`));
236
- console.log(chalk.gray(` 2. Open ${chalk.cyan("http://localhost:3847")} to view and adjust the diagram`));
237
+ console.log(chalk.gray(` 2. Open ${chalk.cyan(`http://localhost:${DEFAULT_PORT}`)} to view and adjust the diagram`));
237
238
  console.log();
238
239
  }
239
240
  catch (error) {
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { loadCredentials, cacheVerifiedTier, resetOfflineActions, checkOfflineAction } from "./auth.js";
3
- const API_BASE = process.env.ARCHBYTE_API_URL ?? "https://api.heartbyte.io";
3
+ import { API_BASE, NETWORK_TIMEOUT_MS } from "./constants.js";
4
4
  /**
5
5
  * Pre-flight license check. Must be called before scan/analyze/generate.
6
6
  *
@@ -44,7 +44,7 @@ export async function requireLicense(action) {
44
44
  "Content-Type": "application/json",
45
45
  },
46
46
  body: JSON.stringify({ action }),
47
- signal: AbortSignal.timeout(5000),
47
+ signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
48
48
  });
49
49
  if (res.status === 401) {
50
50
  console.error();
@@ -112,7 +112,7 @@ export async function recordUsage(meta) {
112
112
  "Content-Type": "application/json",
113
113
  },
114
114
  body: JSON.stringify(meta),
115
- signal: AbortSignal.timeout(5000),
115
+ signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
116
116
  });
117
117
  }
118
118
  catch {
package/dist/cli/run.d.ts CHANGED
@@ -7,6 +7,7 @@ interface RunOptions {
7
7
  verbose?: boolean;
8
8
  force?: boolean;
9
9
  dryRun?: boolean;
10
+ dir?: string;
10
11
  }
11
12
  export declare function handleRun(options: RunOptions): Promise<void>;
12
13
  export {};
package/dist/cli/run.js CHANGED
@@ -1,11 +1,8 @@
1
- import chalk from "chalk";
2
1
  import { handleAnalyze } from "./analyze.js";
3
2
  import { handleServe } from "./serve.js";
3
+ import { DEFAULT_PORT } from "./constants.js";
4
4
  export async function handleRun(options) {
5
- const port = options.port || 3847;
6
- console.log();
7
- console.log(chalk.bold.cyan(" ArchByte Run"));
8
- console.log();
5
+ const port = options.port || DEFAULT_PORT;
9
6
  // 1. Analyze (includes auto-generate)
10
7
  await handleAnalyze({
11
8
  verbose: options.verbose,
@@ -15,11 +12,10 @@ export async function handleRun(options) {
15
12
  apiKey: options.apiKey,
16
13
  force: options.force,
17
14
  dryRun: options.dryRun,
15
+ dir: options.dir,
18
16
  });
19
17
  if (options.dryRun)
20
18
  return;
21
19
  // 2. Serve the UI
22
- console.log(chalk.bold.cyan(" Starting UI..."));
23
- console.log();
24
20
  await handleServe({ port });
25
21
  }
package/dist/cli/serve.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import * as path from "path";
2
2
  import * as fs from "fs";
3
3
  import chalk from "chalk";
4
+ import { DEFAULT_PORT } from "./constants.js";
4
5
  /**
5
6
  * Start the ArchByte UI server
6
7
  */
7
8
  export async function handleServe(options) {
8
9
  const rootDir = process.cwd();
9
- const port = options.port || 3847;
10
+ const port = options.port || DEFAULT_PORT;
10
11
  const diagramPath = options.diagram
11
12
  ? path.resolve(rootDir, options.diagram)
12
13
  : path.join(rootDir, ".archbyte", "architecture.json");
@@ -22,7 +23,7 @@ export async function handleServe(options) {
22
23
  // ignore
23
24
  }
24
25
  }
25
- console.log(chalk.cyan(`🏗️ ArchByte - ${projectName}`));
26
+ console.log(chalk.cyan(`🏗️ ArchByte - See what agents build. As they build it.`));
26
27
  console.log(chalk.gray(`Diagram: ${path.relative(rootDir, diagramPath)}`));
27
28
  console.log(chalk.gray(`Port: ${port}`));
28
29
  console.log();
package/dist/cli/setup.js CHANGED
@@ -5,16 +5,33 @@ import chalk from "chalk";
5
5
  import { resolveModel } from "../agents/runtime/types.js";
6
6
  import { createProvider } from "../agents/providers/router.js";
7
7
  import { select, spinner, confirm } from "./ui.js";
8
+ import { CONFIG_DIR, CONFIG_PATH } from "./constants.js";
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  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
11
  const PROVIDERS = [
13
- { name: "anthropic", label: "Anthropic", hint: "Claude Opus / Sonnet" },
14
- { name: "openai", label: "OpenAI", hint: "GPT-4o / o1" },
12
+ { name: "anthropic", label: "Anthropic", hint: "Claude Sonnet / Opus" },
13
+ { name: "openai", label: "OpenAI", hint: "Codex / GPT-5.2" },
15
14
  { name: "google", label: "Google", hint: "Gemini Flash / Pro" },
16
- { name: "ollama", label: "Ollama", hint: "Local models (no key needed)" },
17
15
  ];
16
+ const PROVIDER_MODELS = {
17
+ anthropic: [
18
+ { id: "", label: "Default (recommended)", hint: "Haiku for fast agents, Sonnet for connections" },
19
+ { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5", hint: "Fastest, cheapest" },
20
+ { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", hint: "Balanced, great quality" },
21
+ { id: "claude-opus-4-6", label: "Claude Opus 4.6", hint: "Most capable" },
22
+ ],
23
+ openai: [
24
+ { id: "", label: "Default (recommended)", hint: "Codex for fast agents, GPT-5.2 for connections" },
25
+ { id: "gpt-5.2-codex", label: "GPT-5.2 Codex", hint: "Fast, coding-optimized" },
26
+ { id: "gpt-5.2", label: "GPT-5.2", hint: "General purpose, thinking model" },
27
+ { id: "o3", label: "o3", hint: "Reasoning model" },
28
+ ],
29
+ google: [
30
+ { id: "", label: "Default (recommended)", hint: "Flash for fast agents, Pro for connections" },
31
+ { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash", hint: "Fastest, cheapest" },
32
+ { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro", hint: "Most capable" },
33
+ ],
34
+ };
18
35
  function loadConfig() {
19
36
  try {
20
37
  if (fs.existsSync(CONFIG_PATH)) {
@@ -75,40 +92,8 @@ function askHidden(prompt) {
75
92
  stdin.on("data", onData);
76
93
  });
77
94
  }
78
- async function fetchOllamaModels(baseUrl) {
95
+ async function validateProviderSilent(providerName, apiKey) {
79
96
  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
97
  const provider = createProvider({ provider: providerName, apiKey });
113
98
  const model = resolveModel(providerName, "fast");
114
99
  await provider.chat({
@@ -123,61 +108,113 @@ async function validateProviderSilent(providerName, apiKey, ollamaBaseUrl) {
123
108
  return false;
124
109
  }
125
110
  }
111
+ function getProfiles(config) {
112
+ return config.profiles ?? {};
113
+ }
126
114
  export async function handleSetup() {
127
115
  console.log();
128
116
  console.log(chalk.bold.cyan("ArchByte Setup"));
129
117
  console.log(chalk.gray("Configure your model provider and API key.\n"));
130
118
  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)}`));
119
+ const profiles = getProfiles(config);
120
+ // Migrate legacy flat config → profiles
121
+ if (!config.profiles && config.provider && config.apiKey) {
122
+ const legacy = config.provider;
123
+ const validNames = PROVIDERS.map((p) => p.name);
124
+ if (validNames.includes(legacy)) {
125
+ // Known provider — migrate to profile
126
+ profiles[legacy] = { apiKey: config.apiKey };
127
+ if (config.model)
128
+ profiles[legacy].model = config.model;
129
+ config.profiles = profiles;
130
+ }
131
+ // Clean up legacy flat keys regardless
132
+ delete config.apiKey;
133
+ delete config.model;
134
+ delete config.ollamaBaseUrl;
135
+ if (!validNames.includes(legacy)) {
136
+ delete config.provider;
137
+ }
138
+ }
139
+ // Show current config + configured profiles
140
+ const configured = Object.keys(profiles).filter((p) => profiles[p]?.apiKey);
141
+ if (configured.length > 0) {
142
+ console.log(chalk.gray(` Active provider: ${config.provider ?? "none"}`));
143
+ for (const p of configured) {
144
+ const active = p === config.provider ? chalk.green(" (active)") : "";
145
+ const model = profiles[p].model ? chalk.gray(` model: ${profiles[p].model}`) : "";
146
+ console.log(chalk.gray(` ${p}: ${maskKey(profiles[p].apiKey)}${model}${active}`));
136
147
  }
137
148
  console.log();
138
149
  }
139
- // Step 1: Choose provider with arrow-key selection
150
+ // Step 1: Choose provider
140
151
  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}`;
152
+ const active = config.provider === p.name ? chalk.green(" (active)") : "";
153
+ const hasKey = profiles[p.name]?.apiKey ? chalk.cyan(" ✓ configured") : "";
154
+ return `${p.label} ${chalk.gray("— " + p.hint)}${hasKey}${active}`;
143
155
  }));
144
156
  const provider = PROVIDERS[idx].name;
145
157
  config.provider = provider;
146
158
  const selected = PROVIDERS[idx];
147
159
  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}`));
160
+ // Step 2: API key (show existing if profile exists)
161
+ const existing = profiles[provider];
162
+ let apiKey;
163
+ if (existing?.apiKey) {
164
+ console.log(chalk.gray(`\n Existing key: ${maskKey(existing.apiKey)}`));
165
+ const keepIdx = await select(" Update API key?", [
166
+ `Keep existing ${chalk.gray("— " + maskKey(existing.apiKey))}`,
167
+ "Enter new key",
168
+ ]);
169
+ if (keepIdx === 0) {
170
+ apiKey = existing.apiKey;
171
+ console.log(chalk.green(` ✓ API key: ${maskKey(apiKey)} (kept)`));
158
172
  }
159
173
  else {
160
- console.log(chalk.yellow(` Warning: no models installed.`));
161
- console.log(chalk.gray(` Run: ollama pull <model-name>`));
174
+ console.log();
175
+ apiKey = await askHidden(chalk.bold(" API key: "));
176
+ if (!apiKey) {
177
+ console.log(chalk.red(" No API key entered. Setup cancelled."));
178
+ process.exit(1);
179
+ }
180
+ console.log(chalk.green(` ✓ API key: ${maskKey(apiKey)}`));
162
181
  }
163
182
  }
164
183
  else {
165
- // Clear Ollama model override — model names are provider-specific
166
- if (config.model) {
167
- delete config.model;
168
- }
169
184
  console.log();
170
- const apiKey = await askHidden(chalk.bold(" API key: "));
185
+ apiKey = await askHidden(chalk.bold(" API key: "));
171
186
  if (!apiKey) {
172
187
  console.log(chalk.red(" No API key entered. Setup cancelled."));
173
188
  process.exit(1);
174
189
  }
175
- config.apiKey = apiKey;
176
190
  console.log(chalk.green(` ✓ API key: ${maskKey(apiKey)}`));
177
191
  }
192
+ // Save to profile
193
+ if (!profiles[provider])
194
+ profiles[provider] = { apiKey: "" };
195
+ profiles[provider].apiKey = apiKey;
196
+ // Step 3: Model selection
197
+ const models = PROVIDER_MODELS[provider];
198
+ if (models) {
199
+ const currentModel = profiles[provider].model;
200
+ const modelIdx = await select("\n Choose a model:", models.map((m) => {
201
+ const current = currentModel === m.id && m.id ? chalk.green(" (current)") : "";
202
+ return `${m.label} ${chalk.gray("— " + m.hint)}${current}`;
203
+ }));
204
+ const chosen = models[modelIdx];
205
+ if (chosen.id) {
206
+ profiles[provider].model = chosen.id;
207
+ console.log(chalk.green(` ✓ Model: ${chosen.label}`));
208
+ }
209
+ else {
210
+ delete profiles[provider].model;
211
+ console.log(chalk.green(` ✓ Model: Default (auto per agent tier)`));
212
+ }
213
+ }
214
+ config.profiles = profiles;
178
215
  // Validate provider with spinner
179
216
  const s = spinner("Validating credentials");
180
- let isValid = await validateProviderSilent(provider, config.apiKey ?? "", config.ollamaBaseUrl);
217
+ let isValid = await validateProviderSilent(provider, apiKey);
181
218
  s.stop(isValid ? "valid" : "invalid", isValid ? "green" : "red");
182
219
  // Retry loop on failure
183
220
  if (!isValid) {
@@ -186,21 +223,23 @@ export async function handleSetup() {
186
223
  if (!await confirm(" Retry?"))
187
224
  break;
188
225
  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
- }
226
+ const newKey = await askHidden(chalk.bold(" API key: "));
227
+ if (newKey) {
228
+ profiles[provider].apiKey = newKey;
229
+ console.log(chalk.green(` ✓ API key: ${maskKey(newKey)}`));
195
230
  }
196
231
  const s2 = spinner("Validating credentials");
197
- isValid = await validateProviderSilent(provider, config.apiKey ?? "", config.ollamaBaseUrl);
232
+ isValid = await validateProviderSilent(provider, profiles[provider].apiKey);
198
233
  s2.stop(isValid ? "valid" : "invalid", isValid ? "green" : "red");
199
234
  }
200
235
  if (!isValid) {
201
236
  console.log(chalk.yellow(" Warning: credentials unverified. Saving anyway."));
202
237
  }
203
238
  }
239
+ // Clean up legacy top-level keys
240
+ delete config.apiKey;
241
+ delete config.model;
242
+ delete config.ollamaBaseUrl;
204
243
  // Save config
205
244
  saveConfig(config);
206
245
  console.log(chalk.gray(`\n Config saved to ${CONFIG_PATH}`));
@@ -236,5 +275,6 @@ export async function handleSetup() {
236
275
  console.log(chalk.green(" Setup complete!"));
237
276
  console.log();
238
277
  console.log(chalk.bold(" Next: ") + chalk.cyan("archbyte run") + chalk.bold(" to analyze your codebase."));
278
+ console.log(chalk.gray(" Run from your project root, or use ") + chalk.cyan("archbyte run -d /path/to/project"));
239
279
  console.log();
240
280
  }
@@ -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
+ }
@@ -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" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archbyte",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "ArchByte - See what agents build. As they build it.",
5
5
  "type": "module",
6
6
  "bin": {