buildwithnexus 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -9,27 +9,215 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/core/config.ts
13
- import dotenv from "dotenv";
12
+ // src/core/dlp.ts
13
+ import crypto from "crypto";
14
+ import fs from "fs";
14
15
  import path from "path";
15
- import os from "os";
16
- function loadApiKeys() {
17
- return {
18
- anthropic: process.env.ANTHROPIC_API_KEY || void 0,
19
- openai: process.env.OPENAI_API_KEY || void 0,
20
- google: process.env.GOOGLE_API_KEY || void 0
21
- };
16
+ function shellEscape(value) {
17
+ if (value.includes("\0")) {
18
+ throw new DlpViolation("Null byte in shell argument");
19
+ }
20
+ return "'" + value.replace(/'/g, "'\\''") + "'";
22
21
  }
23
- function hasAnyKey() {
24
- return !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || process.env.GOOGLE_API_KEY);
22
+ function shellCommand(strings, ...values) {
23
+ let result = strings[0];
24
+ for (let i = 0; i < values.length; i++) {
25
+ result += shellEscape(values[i]) + strings[i + 1];
26
+ }
27
+ return result;
28
+ }
29
+ function redact(text) {
30
+ let result = text;
31
+ for (const pattern of SECRET_PATTERNS) {
32
+ pattern.lastIndex = 0;
33
+ result = result.replace(pattern, "[REDACTED]");
34
+ }
35
+ return result;
36
+ }
37
+ function redactError(err) {
38
+ if (err instanceof Error) {
39
+ const safe = new Error(redact(err.message));
40
+ safe.name = err.name;
41
+ if (err.stack) safe.stack = redact(err.stack);
42
+ return safe;
43
+ }
44
+ return new Error(redact(String(err)));
45
+ }
46
+ function validateKeyValue(keyName, value) {
47
+ if (FORBIDDEN_KEY_CHARS.test(value)) {
48
+ throw new DlpViolation(
49
+ `${keyName} contains characters that are not permitted in API keys`
50
+ );
51
+ }
52
+ if (value.length < 10 || value.length > 256) {
53
+ throw new DlpViolation(
54
+ `${keyName} length out of expected range (10-256 characters)`
55
+ );
56
+ }
57
+ const validator = KEY_VALIDATORS[keyName];
58
+ if (validator && !validator.test(value)) {
59
+ throw new DlpViolation(
60
+ `${keyName} does not match the expected format for this key type`
61
+ );
62
+ }
63
+ }
64
+ function validateAllKeys(keys) {
65
+ const violations = [];
66
+ for (const [name, value] of Object.entries(keys)) {
67
+ if (!value) continue;
68
+ try {
69
+ validateKeyValue(name, value);
70
+ } catch (err) {
71
+ if (err instanceof DlpViolation) violations.push(err.message);
72
+ }
73
+ }
74
+ return violations;
75
+ }
76
+ function computeFileHmac(filePath, secret) {
77
+ const content = fs.readFileSync(filePath);
78
+ return crypto.createHmac("sha256", secret).update(content).digest("hex");
79
+ }
80
+ function sealKeysFile(keysPath, masterSecret) {
81
+ const hmac = computeFileHmac(keysPath, masterSecret);
82
+ fs.writeFileSync(HMAC_PATH, hmac, { mode: 384 });
83
+ }
84
+ function verifyKeysFile(keysPath, masterSecret) {
85
+ const keysExist = fs.existsSync(keysPath);
86
+ const hmacExist = fs.existsSync(HMAC_PATH);
87
+ if (!keysExist && !hmacExist) return true;
88
+ if (keysExist && !hmacExist) return false;
89
+ try {
90
+ const stored = fs.readFileSync(HMAC_PATH, "utf-8").trim();
91
+ const computed = computeFileHmac(keysPath, masterSecret);
92
+ return crypto.timingSafeEqual(
93
+ Buffer.from(stored, "hex"),
94
+ Buffer.from(computed, "hex")
95
+ );
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+ function audit(event, detail = "") {
101
+ try {
102
+ const dir = path.dirname(AUDIT_PATH);
103
+ if (!fs.existsSync(dir)) return;
104
+ if (fs.existsSync(AUDIT_PATH)) {
105
+ const stat = fs.statSync(AUDIT_PATH);
106
+ if (stat.size > MAX_AUDIT_SIZE) {
107
+ const rotated = AUDIT_PATH + ".1";
108
+ if (fs.existsSync(rotated)) fs.unlinkSync(rotated);
109
+ fs.renameSync(AUDIT_PATH, rotated);
110
+ try {
111
+ fs.chmodSync(rotated, 384);
112
+ } catch {
113
+ }
114
+ }
115
+ }
116
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} | ${event} | ${redact(detail)}
117
+ `;
118
+ fs.appendFileSync(AUDIT_PATH, line, { mode: 384 });
119
+ try {
120
+ fs.chmodSync(AUDIT_PATH, 384);
121
+ } catch {
122
+ }
123
+ } catch {
124
+ }
125
+ }
126
+ var NEXUS_HOME, SECRET_PATTERNS, FORBIDDEN_KEY_CHARS, KEY_VALIDATORS, DlpViolation, HMAC_PATH, AUDIT_PATH, MAX_AUDIT_SIZE;
127
+ var init_dlp = __esm({
128
+ "src/core/dlp.ts"() {
129
+ "use strict";
130
+ NEXUS_HOME = path.join(process.env.HOME || "~", ".buildwithnexus");
131
+ SECRET_PATTERNS = [
132
+ /sk-ant-api03-[A-Za-z0-9_-]{20,}/g,
133
+ // Anthropic API key
134
+ /sk-[A-Za-z0-9]{20,}/g,
135
+ // OpenAI API key
136
+ /AIza[A-Za-z0-9_-]{35}/g
137
+ // Google AI API key
138
+ ];
139
+ FORBIDDEN_KEY_CHARS = /[\n\r\t'"\\`${}();&|<>!#%^]/;
140
+ KEY_VALIDATORS = {
141
+ ANTHROPIC_API_KEY: /^sk-ant-[A-Za-z0-9_-]{20,}$/,
142
+ OPENAI_API_KEY: /^sk-[A-Za-z0-9_-]{20,}$/,
143
+ GOOGLE_API_KEY: /^AIza[A-Za-z0-9_-]{35,}$/,
144
+ NEXUS_MASTER_SECRET: /^[A-Za-z0-9_-]{20,64}$/
145
+ };
146
+ DlpViolation = class extends Error {
147
+ constructor(message) {
148
+ super(message);
149
+ this.name = "DlpViolation";
150
+ }
151
+ };
152
+ HMAC_PATH = path.join(NEXUS_HOME, ".keys.hmac");
153
+ AUDIT_PATH = path.join(NEXUS_HOME, "audit.log");
154
+ MAX_AUDIT_SIZE = 10 * 1024 * 1024;
155
+ }
156
+ });
157
+
158
+ // src/core/secrets.ts
159
+ import fs2 from "fs";
160
+ import path2 from "path";
161
+ import crypto2 from "crypto";
162
+ function ensureHome() {
163
+ fs2.mkdirSync(NEXUS_HOME2, { recursive: true, mode: 448 });
164
+ fs2.mkdirSync(path2.join(NEXUS_HOME2, "vm", "images"), { recursive: true, mode: 448 });
165
+ fs2.mkdirSync(path2.join(NEXUS_HOME2, "vm", "configs"), { recursive: true, mode: 448 });
166
+ fs2.mkdirSync(path2.join(NEXUS_HOME2, "vm", "logs"), { recursive: true, mode: 448 });
167
+ fs2.mkdirSync(path2.join(NEXUS_HOME2, "ssh"), { recursive: true, mode: 448 });
168
+ }
169
+ function generateMasterSecret() {
170
+ return crypto2.randomBytes(32).toString("base64url");
171
+ }
172
+ function saveConfig(config) {
173
+ const { masterSecret: _secret, ...safeConfig } = config;
174
+ fs2.writeFileSync(CONFIG_PATH, JSON.stringify(safeConfig, null, 2), { mode: 384 });
175
+ }
176
+ function loadConfig() {
177
+ if (!fs2.existsSync(CONFIG_PATH)) return null;
178
+ return JSON.parse(fs2.readFileSync(CONFIG_PATH, "utf-8"));
179
+ }
180
+ function saveKeys(keys) {
181
+ const violations = validateAllKeys(keys);
182
+ if (violations.length > 0) {
183
+ throw new DlpViolation(`Key validation failed: ${violations.join("; ")}`);
184
+ }
185
+ const lines = Object.entries(keys).filter(([, v]) => v).map(([k, v]) => `${k}=${v}`);
186
+ fs2.writeFileSync(KEYS_PATH, lines.join("\n") + "\n", { mode: 384 });
187
+ sealKeysFile(KEYS_PATH, keys.NEXUS_MASTER_SECRET);
188
+ audit("keys_saved", `${Object.keys(keys).filter((k) => keys[k]).length} keys saved`);
189
+ }
190
+ function loadKeys() {
191
+ if (!fs2.existsSync(KEYS_PATH)) return null;
192
+ const content = fs2.readFileSync(KEYS_PATH, "utf-8");
193
+ const keys = {};
194
+ for (const line of content.split("\n")) {
195
+ const eq = line.indexOf("=");
196
+ if (eq > 0) keys[line.slice(0, eq)] = line.slice(eq + 1);
197
+ }
198
+ const result = keys;
199
+ if (result.NEXUS_MASTER_SECRET && !verifyKeysFile(KEYS_PATH, result.NEXUS_MASTER_SECRET)) {
200
+ audit("keys_tampered", "HMAC mismatch on .env.keys");
201
+ throw new DlpViolation(
202
+ ".env.keys has been modified outside of buildwithnexus. Run 'buildwithnexus keys set' to re-enter your keys, or 'buildwithnexus destroy' to start fresh."
203
+ );
204
+ }
205
+ audit("keys_loaded", `${Object.keys(keys).length} keys loaded`);
206
+ return result;
25
207
  }
26
- function reloadEnv(envPath) {
27
- const resolvedPath = envPath ?? path.join(os.homedir(), ".env.local");
28
- dotenv.config({ path: resolvedPath, override: true });
208
+ function maskKey(key) {
209
+ if (key.length <= 8) return "***";
210
+ const reveal = Math.min(4, Math.floor(key.length * 0.1));
211
+ return key.slice(0, reveal) + "..." + key.slice(-reveal);
29
212
  }
30
- var init_config = __esm({
31
- "src/core/config.ts"() {
213
+ var NEXUS_HOME2, CONFIG_PATH, KEYS_PATH;
214
+ var init_secrets = __esm({
215
+ "src/core/secrets.ts"() {
32
216
  "use strict";
217
+ init_dlp();
218
+ NEXUS_HOME2 = process.env.NEXUS_HOME || path2.join(process.env.HOME || "~", ".buildwithnexus");
219
+ CONFIG_PATH = process.env.NEXUS_CONFIG_PATH || path2.join(NEXUS_HOME2, "config.json");
220
+ KEYS_PATH = process.env.NEXUS_KEYS_PATH || path2.join(NEXUS_HOME2, ".env.keys");
33
221
  }
34
222
  });
35
223
 
@@ -38,1491 +226,1637 @@ var init_command_exports = {};
38
226
  __export(init_command_exports, {
39
227
  deepAgentsInitCommand: () => deepAgentsInitCommand
40
228
  });
41
- import fs from "fs";
42
- import path2 from "path";
43
- import os2 from "os";
44
229
  import * as readline from "readline";
45
230
  async function deepAgentsInitCommand() {
46
- const envPath = path2.join(os2.homedir(), ".env.local");
231
+ const inputHandler = new InputHandler();
232
+ ensureHome();
233
+ let existingKeys = null;
234
+ try {
235
+ existingKeys = loadKeys();
236
+ } catch {
237
+ }
238
+ if (existingKeys) {
239
+ console.log("\nKeys found in ~/.buildwithnexus/.env.keys:");
240
+ if (existingKeys.ANTHROPIC_API_KEY) {
241
+ console.log(` ANTHROPIC_API_KEY: ${maskKey(existingKeys.ANTHROPIC_API_KEY)}`);
242
+ }
243
+ if (existingKeys.OPENAI_API_KEY) {
244
+ console.log(` OPENAI_API_KEY: ${maskKey(existingKeys.OPENAI_API_KEY)}`);
245
+ }
246
+ if (existingKeys.GOOGLE_API_KEY) {
247
+ console.log(` GOOGLE_API_KEY: ${maskKey(existingKeys.GOOGLE_API_KEY)}`);
248
+ }
249
+ const choice = await inputHandler.askQuestion(
250
+ "\nPress Enter to use cached keys, or type 'new' to reconfigure: "
251
+ );
252
+ if (choice.trim().toLowerCase() !== "new") {
253
+ process.env.ANTHROPIC_API_KEY = existingKeys.ANTHROPIC_API_KEY || "";
254
+ if (existingKeys.OPENAI_API_KEY) process.env.OPENAI_API_KEY = existingKeys.OPENAI_API_KEY;
255
+ if (existingKeys.GOOGLE_API_KEY) process.env.GOOGLE_API_KEY = existingKeys.GOOGLE_API_KEY;
256
+ console.log("Using cached keys.");
257
+ inputHandler.close();
258
+ return;
259
+ }
260
+ }
47
261
  console.log(
48
- "Please provide your LLM API keys:\n(You can set these later by editing .env.local)\n"
262
+ "\nPlease provide your LLM API keys:\n(Stored securely in ~/.buildwithnexus/.env.keys)\n"
49
263
  );
50
- const anthropicKey = await askQuestion(
264
+ const anthropicKey = await inputHandler.askQuestion(
51
265
  "ANTHROPIC_API_KEY (Claude - optional, press Enter to skip): "
52
266
  );
53
- const openaiKey = await askQuestion(
267
+ const openaiKey = await inputHandler.askQuestion(
54
268
  "OPENAI_API_KEY (GPT - optional, press Enter to skip): "
55
269
  );
56
- const googleKey = await askQuestion(
270
+ const googleKey = await inputHandler.askQuestion(
57
271
  "GOOGLE_API_KEY (Gemini - optional, press Enter to skip): "
58
272
  );
59
273
  if (!anthropicKey && !openaiKey && !googleKey) {
60
- console.log("Error: At least one API key must be provided.");
274
+ const anthropicStatus = anthropicKey ? "provided" : "empty";
275
+ const openaiStatus = openaiKey ? "provided" : "empty";
276
+ const googleStatus = googleKey ? "provided" : "empty";
277
+ console.log(
278
+ `Error: API keys status: Anthropic [${anthropicStatus}], OpenAI [${openaiStatus}], Google [${googleStatus}]. Please provide at least one.`
279
+ );
280
+ inputHandler.close();
61
281
  return;
62
282
  }
63
- const backendUrl = await askQuestion("Backend URL (http://localhost:4200): ") || "http://localhost:4200";
64
- const dashboardPort = await askQuestion("Dashboard port (4201): ") || "4201";
65
- const envContent = `# Nexus Configuration
66
- # Generated by buildwithnexus init
67
-
68
- # LLM API Keys
69
- ${anthropicKey ? `ANTHROPIC_API_KEY=${anthropicKey}` : "# ANTHROPIC_API_KEY="}
70
- ${openaiKey ? `OPENAI_API_KEY=${openaiKey}` : "# OPENAI_API_KEY="}
71
- ${googleKey ? `GOOGLE_API_KEY=${googleKey}` : "# GOOGLE_API_KEY="}
72
-
73
- # Backend
74
- BACKEND_URL=${backendUrl}
75
- DASHBOARD_PORT=${dashboardPort}
76
-
77
- # Optional
78
- # LOG_LEVEL=debug
79
- `;
80
- fs.writeFileSync(envPath, envContent, { mode: 384 });
81
- reloadEnv(envPath);
82
- console.log("Configuration saved to .env.local and loaded into environment.");
83
- }
84
- function setupInputHandling() {
85
- return new Promise((resolve) => {
86
- if (isSetup) {
87
- resolve();
88
- return;
89
- }
90
- isSetup = true;
91
- if (process.stdin.isTTY) {
92
- resolve();
93
- } else {
94
- const rl = readline.createInterface({
95
- input: process.stdin,
96
- output: process.stdout,
97
- terminal: false
98
- });
99
- rl.on("line", (line) => {
100
- inputLines.push(line);
101
- });
102
- rl.on("close", () => {
103
- resolve();
104
- });
105
- }
106
- });
107
- }
108
- async function askQuestion(prompt) {
109
- await setupInputHandling();
110
- if (process.stdin.isTTY) {
111
- return new Promise((resolve) => {
112
- const rl = readline.createInterface({
113
- input: process.stdin,
114
- output: process.stdout
115
- });
116
- rl.question(prompt, (answer) => {
117
- rl.close();
118
- resolve(answer);
119
- });
120
- });
121
- } else {
122
- process.stdout.write(prompt);
123
- const answer = inputLines[lineIndex] || "";
124
- lineIndex++;
125
- console.log(answer);
126
- return answer;
127
- }
283
+ const masterSecret = generateMasterSecret();
284
+ const keys = {
285
+ ANTHROPIC_API_KEY: anthropicKey,
286
+ OPENAI_API_KEY: openaiKey || void 0,
287
+ GOOGLE_API_KEY: googleKey || void 0,
288
+ NEXUS_MASTER_SECRET: masterSecret
289
+ };
290
+ saveKeys(keys);
291
+ if (anthropicKey) process.env.ANTHROPIC_API_KEY = anthropicKey;
292
+ if (openaiKey) process.env.OPENAI_API_KEY = openaiKey;
293
+ if (googleKey) process.env.GOOGLE_API_KEY = googleKey;
294
+ console.log("Configuration saved to ~/.buildwithnexus/.env.keys and loaded into environment.");
295
+ inputHandler.close();
128
296
  }
129
- var inputLines, lineIndex, isSetup;
297
+ var InputHandler;
130
298
  var init_init_command = __esm({
131
299
  "src/cli/init-command.ts"() {
132
300
  "use strict";
133
- init_config();
134
- inputLines = [];
135
- lineIndex = 0;
136
- isSetup = false;
301
+ init_secrets();
302
+ InputHandler = class {
303
+ inputLines = [];
304
+ lineIndex = 0;
305
+ isSetup = false;
306
+ rl = null;
307
+ setupInputHandling() {
308
+ return new Promise((resolve) => {
309
+ if (this.isSetup) {
310
+ resolve();
311
+ return;
312
+ }
313
+ this.isSetup = true;
314
+ if (process.stdin.isTTY) {
315
+ this.rl = readline.createInterface({
316
+ input: process.stdin,
317
+ output: process.stdout
318
+ });
319
+ resolve();
320
+ } else {
321
+ const rl = readline.createInterface({
322
+ input: process.stdin,
323
+ output: process.stdout,
324
+ terminal: false
325
+ });
326
+ rl.on("line", (line) => {
327
+ this.inputLines.push(line);
328
+ });
329
+ rl.on("close", () => {
330
+ resolve();
331
+ });
332
+ }
333
+ });
334
+ }
335
+ async askQuestion(prompt) {
336
+ await this.setupInputHandling();
337
+ if (process.stdin.isTTY) {
338
+ return new Promise((resolve) => {
339
+ this.rl.question(prompt, (answer) => {
340
+ resolve(answer);
341
+ });
342
+ });
343
+ } else {
344
+ process.stdout.write(prompt);
345
+ const answer = this.inputLines[this.lineIndex] || "";
346
+ this.lineIndex++;
347
+ console.log(answer);
348
+ return answer;
349
+ }
350
+ }
351
+ close() {
352
+ if (this.rl) {
353
+ this.rl.close();
354
+ this.rl = null;
355
+ }
356
+ }
357
+ };
137
358
  }
138
359
  });
139
360
 
140
- // src/ui/banner.ts
141
- var banner_exports = {};
142
- __export(banner_exports, {
143
- showBanner: () => showBanner,
144
- showCompletion: () => showCompletion,
145
- showPhase: () => showPhase,
146
- showSecurityPosture: () => showSecurityPosture
361
+ // src/ui/logger.ts
362
+ import chalk2 from "chalk";
363
+ var log;
364
+ var init_logger = __esm({
365
+ "src/ui/logger.ts"() {
366
+ "use strict";
367
+ log = {
368
+ step(msg) {
369
+ console.log(chalk2.cyan(" \u2192 ") + msg);
370
+ },
371
+ success(msg) {
372
+ console.log(chalk2.green(" \u2713 ") + msg);
373
+ },
374
+ error(msg) {
375
+ console.error(chalk2.red(" \u2717 ") + msg);
376
+ },
377
+ warn(msg) {
378
+ console.log(chalk2.yellow(" \u26A0 ") + msg);
379
+ },
380
+ dim(msg) {
381
+ console.log(chalk2.dim(" " + msg));
382
+ },
383
+ detail(label, value) {
384
+ console.log(chalk2.dim(" " + label + ": ") + value);
385
+ },
386
+ progress(current, total, label) {
387
+ const pct = Math.round(current / total * 100);
388
+ const filled = Math.round(current / total * 20);
389
+ const bar = chalk2.cyan("\u2588".repeat(filled)) + chalk2.dim("\u2591".repeat(20 - filled));
390
+ process.stdout.write(`\r [${bar}] ${chalk2.bold(`${pct}%`)} ${chalk2.dim(label)}`);
391
+ if (current >= total) process.stdout.write("\n");
392
+ }
393
+ };
394
+ }
147
395
  });
148
- import chalk5 from "chalk";
149
- import { readFileSync } from "fs";
150
- import { dirname, join } from "path";
151
- import { fileURLToPath as fileURLToPath2 } from "url";
152
- function getVersion() {
153
- try {
154
- const __dirname2 = dirname(fileURLToPath2(import.meta.url));
155
- const packagePath = join(__dirname2, "..", "package.json");
156
- const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
157
- return packageJson.version;
158
- } catch {
159
- return true ? "0.7.1" : "0.0.0-unknown";
396
+
397
+ // src/core/platform.ts
398
+ import os2 from "os";
399
+ function detectPlatform() {
400
+ const platform = os2.platform();
401
+ const arch = os2.arch();
402
+ if (platform === "darwin") {
403
+ return {
404
+ os: "mac",
405
+ arch: arch === "arm64" ? "arm64" : "x64",
406
+ dockerPlatform: arch === "arm64" ? "linux/arm64" : "linux/amd64"
407
+ };
160
408
  }
409
+ if (platform === "linux") {
410
+ return {
411
+ os: "linux",
412
+ arch: arch === "arm64" ? "arm64" : "x64",
413
+ dockerPlatform: arch === "arm64" ? "linux/arm64" : "linux/amd64"
414
+ };
415
+ }
416
+ if (platform === "win32") {
417
+ return {
418
+ os: "windows",
419
+ arch: "x64",
420
+ dockerPlatform: "linux/amd64"
421
+ };
422
+ }
423
+ throw new Error(`Unsupported platform: ${platform} ${arch}`);
161
424
  }
162
- function showBanner() {
163
- console.log(BANNER);
164
- console.log(chalk5.dim(` v${getVersion()} \xB7 buildwithnexus.dev
165
- `));
166
- }
167
- function showPhase(phase, total, description) {
168
- const progress = chalk5.cyan(`[${phase}/${total}]`);
169
- console.log(`
170
- ${progress} ${chalk5.bold(description)}`);
171
- }
172
- function showSecurityPosture() {
173
- const lines = [
174
- "",
175
- chalk5.bold(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"),
176
- chalk5.bold(" \u2551 ") + chalk5.bold.green("Security Posture") + chalk5.bold(" \u2551"),
177
- chalk5.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
178
- chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" Triple-nested isolation: Host \u2192 VM \u2192 Docker \u2192 KVM".padEnd(54)) + chalk5.bold("\u2551"),
179
- chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" Network hardened: UFW deny-all, allow 22/80/443/4200".padEnd(54)) + chalk5.bold("\u2551"),
180
- chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" All databases encrypted at rest (AES-256-CBC)".padEnd(54)) + chalk5.bold("\u2551"),
181
- chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" API keys never embedded in VM \u2014 delivered via SCP".padEnd(54)) + chalk5.bold("\u2551"),
182
- chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" SSH-only communication (no exposed network ports)".padEnd(54)) + chalk5.bold("\u2551"),
183
- chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" DLP: secret detection, shell escaping, output redaction".padEnd(54)) + chalk5.bold("\u2551"),
184
- chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" HMAC integrity verification on all key files".padEnd(54)) + chalk5.bold("\u2551"),
185
- chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" Docker: --read-only, no-new-privileges, cap-drop=ALL".padEnd(54)) + chalk5.bold("\u2551"),
186
- chalk5.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
187
- chalk5.bold(" \u2551 ") + chalk5.dim("Full details: https://buildwithnexus.dev/security".padEnd(55)) + chalk5.bold("\u2551"),
188
- chalk5.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"),
189
- ""
190
- ];
191
- console.log(lines.join("\n"));
192
- }
193
- function showCompletion(urls) {
194
- const lines = [
195
- "",
196
- chalk5.green(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"),
197
- chalk5.green(" \u2551 ") + chalk5.bold.green("NEXUS Runtime is Live!") + chalk5.green(" \u2551"),
198
- chalk5.green(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
199
- chalk5.green(" \u2551 ") + chalk5.white(`Connect: ${urls.ssh}`.padEnd(55)) + chalk5.green("\u2551")
200
- ];
201
- if (urls.remote) {
202
- lines.push(chalk5.green(" \u2551 ") + chalk5.white(`Remote: ${urls.remote}`.padEnd(55)) + chalk5.green("\u2551"));
203
- }
204
- lines.push(
205
- chalk5.green(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
206
- chalk5.green(" \u2551 ") + chalk5.dim("Quick Start:".padEnd(55)) + chalk5.green("\u2551"),
207
- chalk5.green(" \u2551 ") + chalk5.white(" buildwithnexus - Interactive shell".padEnd(55)) + chalk5.green("\u2551"),
208
- chalk5.green(" \u2551 ") + chalk5.white(" buildwithnexus brainstorm - Brainstorm ideas".padEnd(55)) + chalk5.green("\u2551"),
209
- chalk5.green(" \u2551 ") + chalk5.white(" buildwithnexus status - Check health".padEnd(55)) + chalk5.green("\u2551"),
210
- chalk5.green(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
211
- chalk5.green(" \u2551 ") + chalk5.dim("All commands:".padEnd(55)) + chalk5.green("\u2551"),
212
- chalk5.green(" \u2551 ") + chalk5.white(" buildwithnexus stop/start/update/logs/ssh/destroy".padEnd(55)) + chalk5.green("\u2551"),
213
- chalk5.green(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"),
214
- ""
215
- );
216
- console.log(lines.join("\n"));
217
- }
218
- var BANNER;
219
- var init_banner = __esm({
220
- "src/ui/banner.ts"() {
425
+ var init_platform = __esm({
426
+ "src/core/platform.ts"() {
221
427
  "use strict";
222
- BANNER = `
223
- \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
224
- \u2551 ${chalk5.bold.cyan("B U I L D W I T H N E X U S")} \u2551
225
- \u2551 \u2551
226
- \u2551 Autonomous Agent Orchestration \u2551
227
- \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
228
- `;
229
428
  }
230
429
  });
231
430
 
232
- // src/bin.ts
233
- init_init_command();
234
- import { program } from "commander";
235
-
236
- // src/cli/tui.ts
237
- import chalk from "chalk";
238
- var colors = {
239
- accent: chalk.hex("#7D56F4"),
240
- // Brand purple
241
- success: chalk.hex("#00FF87"),
242
- // Bright green
243
- warning: chalk.hex("#FFB86C"),
244
- // Amber
245
- info: chalk.hex("#8BE9FD"),
246
- // Cyan
247
- muted: chalk.gray,
248
- // Adaptive gray
249
- error: chalk.red
250
- };
251
- var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
252
- var STATUS_SYMBOLS = {
253
- done: colors.success("\u2714"),
254
- active: colors.info("\u25C9"),
255
- pending: colors.muted("\u25CB"),
256
- error: colors.error("\u2716")
257
- };
258
- var TUI = class {
259
- taskStartTime = 0;
260
- eventCount = 0;
261
- spinnerIndex = 0;
262
- getSpinner() {
263
- const frame = SPINNER_FRAMES[this.spinnerIndex % SPINNER_FRAMES.length];
264
- this.spinnerIndex++;
265
- return frame;
266
- }
267
- displayHeader(task, agent) {
268
- console.clear();
269
- const headerBox = this.makeRoundedBox(
270
- colors.accent("\u{1F680} NEXUS - Autonomous Agent Orchestration"),
271
- 62,
272
- colors.accent
273
- );
274
- console.log(headerBox);
275
- console.log("");
276
- console.log(colors.muted("Task") + colors.muted(": ") + chalk.white(task));
277
- console.log(colors.muted("Agent") + colors.muted(": ") + colors.info(agent));
278
- console.log("");
279
- this.taskStartTime = Date.now();
280
- }
281
- displayConnecting() {
282
- console.log(`${this.getSpinner()} ${colors.warning("Connecting to backend...")}`);
431
+ // src/core/docker.ts
432
+ var docker_exports = {};
433
+ __export(docker_exports, {
434
+ dockerExec: () => dockerExec,
435
+ imageExistsLocally: () => imageExistsLocally,
436
+ installDocker: () => installDocker,
437
+ isDockerInstalled: () => isDockerInstalled,
438
+ isNexusRunning: () => isNexusRunning,
439
+ pullImage: () => pullImage,
440
+ startBackend: () => startBackend,
441
+ startNexus: () => startNexus,
442
+ stopNexus: () => stopNexus
443
+ });
444
+ import { existsSync } from "fs";
445
+ import { execa } from "execa";
446
+ async function isDockerInstalled() {
447
+ try {
448
+ await execa("docker", ["info"]);
449
+ return true;
450
+ } catch {
451
+ return false;
283
452
  }
284
- displayConnected(runId) {
285
- console.log(`${STATUS_SYMBOLS.done} Connected \u2022 ${colors.muted(`run: ${runId}`)}`);
286
- console.log("");
453
+ }
454
+ async function isDockerInstalledButNotRunning() {
455
+ try {
456
+ await execa("docker", ["--version"]);
457
+ return true;
458
+ } catch {
459
+ return false;
287
460
  }
288
- displayStreamStart() {
289
- console.log(chalk.bold(colors.accent("\u{1F4E1} Streaming Events")));
290
- console.log("");
461
+ }
462
+ function dockerDesktopExists() {
463
+ return existsSync(DOCKER_DESKTOP_APP_PATH);
464
+ }
465
+ async function ensureHomebrew() {
466
+ try {
467
+ await execa("which", ["brew"]);
468
+ log.dim("Homebrew is already installed.");
469
+ return;
470
+ } catch {
291
471
  }
292
- displayPlan(task, steps) {
293
- console.log("");
294
- const planHeader = colors.accent("Plan Breakdown");
295
- const planLines = [];
296
- steps.forEach((step, i) => {
297
- planLines.push(` ${STATUS_SYMBOLS.pending} ${chalk.white(`Step ${i + 1}:`)} ${step}`);
472
+ log.step("Installing Homebrew...");
473
+ try {
474
+ await execa("/bin/bash", [
475
+ "-c",
476
+ "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
477
+ ], {
478
+ stdio: "inherit",
479
+ env: { ...process.env, NONINTERACTIVE: "1" }
298
480
  });
299
- const planBox = this.makeDoubleBox(planHeader, planLines.join("\n"), 62, colors.accent);
300
- console.log(planBox);
301
- console.log("");
302
- }
303
- displayEvent(type, data) {
304
- this.eventCount++;
305
- const content = data["content"] || "";
306
- if (type === "agent_working") {
307
- const agent = data["agent"] || "Agent";
308
- const agentTask = data["task"] || "";
309
- console.log("");
310
- console.log(` ${colors.info("\u{1F464}")} ${colors.info(chalk.bold(agent))}`);
311
- console.log(` ${colors.muted("\u2192")} ${agentTask}`);
312
- return;
313
- }
314
- if (type === "agent_result") {
315
- const result = data["result"] || "";
316
- let displayResult = result;
317
- if (displayResult.length > 100) {
318
- displayResult = displayResult.substring(0, 97) + "...";
319
- }
320
- console.log(` ${STATUS_SYMBOLS.done} ${chalk.white(displayResult)}`);
321
- return;
322
- }
323
- const eventConfig = {
324
- thought: { icon: "\u{1F4AD}", color: colors.info },
325
- action: { icon: "\u26A1", color: colors.warning },
326
- observation: { icon: "\u2713", color: colors.success },
327
- started: { icon: "\u25B6", color: colors.info },
328
- done: { icon: "\u2728", color: colors.success },
329
- execution_complete: { icon: "\u2728", color: colors.success },
330
- error: { icon: "\u2716", color: colors.error }
331
- };
332
- const config = eventConfig[type] || { icon: "\u25CF", color: colors.muted };
333
- let displayContent = content;
334
- if (displayContent.length > 100) {
335
- displayContent = displayContent.substring(0, 97) + "...";
336
- }
337
- console.log(` ${config.icon} ${config.color(displayContent)}`);
338
- }
339
- displayResults(summary, todosCompleted) {
340
- console.log("");
341
- console.log(colors.success("\u2501".repeat(60)));
342
- console.log(colors.success.bold("\u2728 Execution Complete"));
343
- console.log("");
344
- const lines = summary.split("\n");
345
- for (const line of lines) {
346
- console.log(` ${colors.success("\u2502")} ${chalk.white(line)}`);
347
- }
348
- console.log("");
349
- console.log(` ${colors.muted(`${todosCompleted} subtask(s) completed`)}`);
350
- console.log("");
351
- }
352
- displayError(error) {
353
- console.log("");
354
- console.log(colors.error("\u274C Error"));
355
- console.log(colors.error(error));
356
- console.log("");
357
- }
358
- displayComplete(duration) {
359
- const seconds = (duration / 1e3).toFixed(1);
360
- console.log("");
361
- console.log(colors.success.bold(`\u2728 Complete in ${seconds}s`));
362
- console.log(colors.muted(`${this.eventCount} event(s) streamed`));
363
- console.log("");
481
+ } catch {
482
+ throw new Error(
483
+ 'Failed to install Homebrew automatically.\n\n Install Homebrew manually:\n /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"\n\n Then re-run:\n buildwithnexus init'
484
+ );
364
485
  }
365
- displayBox(title, content) {
366
- const box = this.makeRoundedBox(title, 60, colors.accent);
367
- console.log(box);
368
- const lines = content.split("\n");
369
- for (const line of lines) {
370
- const padded = line.substring(0, 56).padEnd(56);
371
- console.log(` ${padded}`);
372
- }
373
- console.log("");
486
+ try {
487
+ await execa("which", ["brew"]);
488
+ } catch {
489
+ const armPath = "/opt/homebrew/bin";
490
+ const intelPath = "/usr/local/bin";
491
+ process.env.PATH = `${armPath}:${intelPath}:${process.env.PATH}`;
492
+ log.dim("Added Homebrew paths to PATH for this session.");
374
493
  }
375
- getElapsedTime() {
376
- return Date.now() - this.taskStartTime;
494
+ try {
495
+ await execa("brew", ["--version"]);
496
+ log.success("Homebrew installed successfully.");
497
+ } catch {
498
+ throw new Error(
499
+ "Homebrew was installed but is not available on PATH.\n\n Try opening a new terminal and re-running:\n buildwithnexus init"
500
+ );
377
501
  }
378
- displayModeBar(current) {
379
- const modes = ["PLAN", "BUILD", "BRAINSTORM"];
380
- const modeConfig = {
381
- PLAN: { icon: "\u{1F4CB}", color: colors.info },
382
- BUILD: { icon: "\u2699\uFE0F", color: colors.success },
383
- BRAINSTORM: { icon: "\u{1F4A1}", color: chalk.blue }
384
- };
385
- const parts = modes.map((m) => {
386
- const config = modeConfig[m];
387
- const label = `${config.icon} ${m}`;
388
- if (m === current) {
389
- return config.color.bold(label);
390
- }
391
- return colors.muted(label);
392
- });
393
- console.log(parts.join(colors.muted(" \u2022 ")));
394
- console.log(colors.muted("[s] switch mode"));
395
- console.log("");
396
- }
397
- displayModeHeader(mode) {
398
- const config = {
399
- PLAN: {
400
- icon: "\u{1F4CB}",
401
- desc: "Break down and review steps",
402
- color: colors.info
403
- },
404
- BUILD: {
405
- icon: "\u2699\uFE0F",
406
- desc: "Execute with live streaming",
407
- color: colors.success
408
- },
409
- BRAINSTORM: {
410
- icon: "\u{1F4A1}",
411
- desc: "Free-form Q&A and exploration",
412
- color: chalk.blue
502
+ }
503
+ async function installDocker(platform) {
504
+ const p = platform ?? detectPlatform();
505
+ switch (p.os) {
506
+ case "mac": {
507
+ if (await isDockerInstalled()) {
508
+ log.success("Docker is already running.");
509
+ return;
413
510
  }
414
- };
415
- const c = config[mode];
416
- console.log("");
417
- console.log(c.color.bold(`${c.icon} ${mode}`));
418
- console.log(colors.muted(c.desc));
419
- console.log("");
420
- }
421
- displaySuggestedMode(mode, task) {
422
- const modeColor = {
423
- PLAN: colors.info,
424
- BUILD: colors.success,
425
- BRAINSTORM: chalk.blue
426
- };
427
- const taskPreview = task.length > 45 ? task.substring(0, 42) + "..." : task;
428
- console.log("");
429
- console.log(
430
- colors.muted("Suggested: ") + modeColor[mode].bold(mode) + colors.muted(` for "${taskPreview}"`)
431
- );
432
- }
433
- displayBrainstormResponse(response) {
434
- console.log("");
435
- console.log(chalk.blue.bold("\u{1F4A1} Ideas & Analysis"));
436
- console.log("");
437
- const lines = response.split("\n");
438
- for (const line of lines) {
439
- if (line.trim()) {
440
- console.log(` ${chalk.white(line)}`);
511
+ if (dockerDesktopExists()) {
512
+ log.step(`Docker Desktop found at ${DOCKER_DESKTOP_APP_PATH} but not running. Attempting to start...`);
513
+ let launched = false;
514
+ log.dim(`Trying: open ${DOCKER_DESKTOP_APP_PATH}`);
515
+ try {
516
+ await execa("open", [DOCKER_DESKTOP_APP_PATH]);
517
+ launched = true;
518
+ log.dim(`Launch command succeeded via ${DOCKER_DESKTOP_APP_PATH}`);
519
+ } catch {
520
+ log.warn(`Could not launch via ${DOCKER_DESKTOP_APP_PATH} \u2014 trying fallback...`);
521
+ }
522
+ if (!launched) {
523
+ log.dim("Trying: open -a Docker");
524
+ try {
525
+ await execa("open", ["-a", "Docker"]);
526
+ launched = true;
527
+ log.dim("Launch command succeeded via open -a Docker");
528
+ } catch {
529
+ log.warn("Both launch attempts failed.");
530
+ }
531
+ }
532
+ if (launched) {
533
+ log.step("Docker Desktop is starting up. Waiting for the daemon to be ready (up to 120s)...");
534
+ try {
535
+ await waitForDockerDaemon(12e4);
536
+ return;
537
+ } catch {
538
+ log.warn("Docker Desktop was launched but the daemon did not become ready in time.");
539
+ }
540
+ } else {
541
+ log.warn("Could not launch Docker Desktop. Will fall back to reinstalling via Homebrew.");
542
+ }
441
543
  } else {
442
- console.log("");
544
+ log.step(`Docker Desktop not found at ${DOCKER_DESKTOP_APP_PATH}.`);
443
545
  }
546
+ log.step("Installing Docker Desktop via Homebrew...");
547
+ await ensureHomebrew();
548
+ try {
549
+ await execa("brew", ["install", "--cask", "docker"], {
550
+ stdio: "inherit",
551
+ timeout: 6e4
552
+ });
553
+ } catch (err) {
554
+ const e = err;
555
+ if (e.killed && e.signal === "SIGINT") {
556
+ throw new Error("Docker installation cancelled by user (Ctrl+C)");
557
+ }
558
+ if (e.timedOut) {
559
+ throw new Error(
560
+ "Docker installation via Homebrew timed out after 60 seconds.\n\n The password prompt may be waiting for input. Try installing manually:\n brew install --cask docker\n\n After installing, re-run:\n buildwithnexus init"
561
+ );
562
+ }
563
+ throw new Error(
564
+ "Failed to install Docker via Homebrew.\n\n Try installing Docker Desktop manually:\n https://www.docker.com/products/docker-desktop\n\n After installing, re-run:\n buildwithnexus init"
565
+ );
566
+ }
567
+ log.step("Launching Docker Desktop...");
568
+ let postInstallLaunched = false;
569
+ log.dim(`Trying: open ${DOCKER_DESKTOP_APP_PATH}`);
570
+ try {
571
+ await execa("open", [DOCKER_DESKTOP_APP_PATH]);
572
+ postInstallLaunched = true;
573
+ log.dim(`Launch command succeeded via ${DOCKER_DESKTOP_APP_PATH}`);
574
+ } catch {
575
+ log.warn(`Could not launch via ${DOCKER_DESKTOP_APP_PATH} \u2014 trying fallback...`);
576
+ }
577
+ if (!postInstallLaunched) {
578
+ log.dim("Trying: open -a Docker");
579
+ try {
580
+ await execa("open", ["-a", "Docker"]);
581
+ postInstallLaunched = true;
582
+ log.dim("Launch command succeeded via open -a Docker");
583
+ } catch {
584
+ log.warn("Both launch attempts failed after install.");
585
+ }
586
+ }
587
+ if (!postInstallLaunched) {
588
+ throw new Error(
589
+ "Docker Desktop was installed but could not be started automatically.\n\n Next steps:\n 1. Open Docker Desktop manually from your Applications folder\n 2. Wait for the whale icon to appear in the menu bar\n 3. Re-run: buildwithnexus init"
590
+ );
591
+ }
592
+ log.step("Docker Desktop is starting up. Waiting for the daemon to be ready (up to 120s)...");
593
+ await waitForDockerDaemon(12e4);
594
+ break;
444
595
  }
445
- console.log("");
446
- }
447
- displayPermissionPrompt(message) {
448
- return colors.accent.bold(message) + colors.muted(" (y/n) ");
449
- }
450
- makeRoundedBox(title, width, borderColor) {
451
- const lines = [];
452
- const innerWidth = width - 4;
453
- const titlePadded = ` ${title} `.padEnd(innerWidth).substring(0, innerWidth);
454
- lines.push(borderColor("\u256D" + "\u2500".repeat(innerWidth) + "\u256E"));
455
- lines.push(borderColor("\u2502") + chalk.bold(titlePadded) + borderColor("\u2502"));
456
- lines.push(borderColor("\u2570" + "\u2500".repeat(innerWidth) + "\u256F"));
457
- return lines.join("\n");
458
- }
459
- makeDoubleBox(title, content, width, borderColor) {
460
- const lines = [];
461
- const innerWidth = width - 4;
462
- const titlePadded = ` ${title} `.padEnd(innerWidth).substring(0, innerWidth);
463
- lines.push(borderColor("\u2554" + "\u2550".repeat(innerWidth) + "\u2557"));
464
- lines.push(borderColor("\u2551") + chalk.bold(titlePadded) + borderColor("\u2551"));
465
- lines.push(borderColor("\u2560" + "\u2550".repeat(innerWidth) + "\u2563"));
466
- const contentLines = content.split("\n");
467
- for (const line of contentLines) {
468
- const padded = line.substring(0, innerWidth - 2).padEnd(innerWidth - 2);
469
- lines.push(borderColor("\u2551") + " " + padded + borderColor("\u2551"));
470
- }
471
- lines.push(borderColor("\u255A" + "\u2550".repeat(innerWidth) + "\u255D"));
472
- return lines.join("\n");
473
- }
474
- };
475
- var tui = new TUI();
476
-
477
- // src/cli/run-command.ts
478
- init_config();
479
- async function runCommand(task, options) {
480
- const backendUrl = process.env.BACKEND_URL || "http://localhost:4200";
481
- tui.displayHeader(task, options.agent);
482
- tui.displayConnecting();
483
- try {
484
- let healthOk = false;
485
- try {
486
- const healthResponse = await fetch(`${backendUrl}/health`);
487
- healthOk = healthResponse.ok;
488
- } catch {
489
- }
490
- if (!healthOk) {
491
- console.error(
492
- "Backend not responding. Start it with:\n buildwithnexus server"
493
- );
494
- process.exit(1);
495
- }
496
- const keys = loadApiKeys();
497
- const response = await fetch(`${backendUrl}/api/run`, {
498
- method: "POST",
499
- headers: { "Content-Type": "application/json" },
500
- body: JSON.stringify({
501
- task,
502
- agent_role: options.agent,
503
- agent_goal: options.goal || "",
504
- api_key: keys.anthropic || "",
505
- openai_api_key: keys.openai || "",
506
- google_api_key: keys.google || ""
507
- })
508
- });
509
- if (!response.ok) {
510
- console.error("Backend error");
511
- console.error(await response.text());
512
- process.exit(1);
596
+ case "linux": {
597
+ const linuxBinaryExists = await isDockerInstalledButNotRunning();
598
+ if (linuxBinaryExists) {
599
+ log.step("Docker is installed but the daemon is not running.");
600
+ log.step("Starting Docker daemon...");
601
+ try {
602
+ await execa("sudo", ["systemctl", "start", "docker"], { stdio: "inherit" });
603
+ log.dim("Started Docker daemon via systemctl.");
604
+ } catch {
605
+ try {
606
+ await execa("sudo", ["service", "docker", "start"], { stdio: "inherit" });
607
+ log.dim("Started Docker daemon via service command.");
608
+ } catch {
609
+ throw new Error(
610
+ "Docker is installed but the daemon could not be started.\n\n Try starting it manually:\n sudo systemctl start docker\n\n Then re-run:\n buildwithnexus init"
611
+ );
612
+ }
613
+ }
614
+ log.step("Waiting for Docker...");
615
+ await waitForDockerDaemon(3e4);
616
+ return;
617
+ }
618
+ log.step("Installing Docker...");
619
+ log.warn("This may require your sudo password.");
620
+ log.dim("Running official Docker install script from https://get.docker.com ...");
621
+ try {
622
+ const { stdout: script } = await execa("curl", ["-fsSL", "https://get.docker.com"]);
623
+ await execa("sudo", ["sh", "-c", script], { stdio: "inherit" });
624
+ log.success("Docker installed successfully.");
625
+ } catch {
626
+ throw new Error(
627
+ "Failed to install Docker on Linux.\n\n Try installing manually:\n curl -fsSL https://get.docker.com | sudo sh\n\n After installing, re-run:\n buildwithnexus init"
628
+ );
629
+ }
630
+ log.dim("Adding current user to docker group...");
631
+ try {
632
+ const user = (await execa("whoami")).stdout.trim();
633
+ await execa("sudo", ["usermod", "-aG", "docker", user]);
634
+ log.dim(`Added user '${user}' to docker group (may require re-login for effect).`);
635
+ } catch {
636
+ log.warn("Could not add user to docker group. You may need sudo for docker commands.");
637
+ }
638
+ log.step("Starting Docker daemon...");
639
+ try {
640
+ await execa("sudo", ["systemctl", "start", "docker"], { stdio: "inherit" });
641
+ log.dim("Started Docker daemon via systemctl.");
642
+ } catch {
643
+ try {
644
+ await execa("sudo", ["service", "docker", "start"], { stdio: "inherit" });
645
+ log.dim("Started Docker daemon via service command.");
646
+ } catch {
647
+ throw new Error(
648
+ "Docker was installed but the daemon could not be started.\n\n Try starting it manually:\n sudo systemctl start docker\n\n Then re-run:\n buildwithnexus init"
649
+ );
650
+ }
651
+ }
652
+ log.step("Waiting for Docker...");
653
+ await waitForDockerDaemon(3e4);
654
+ break;
513
655
  }
514
- const { run_id } = await response.json();
515
- tui.displayConnected(run_id);
516
- tui.displayStreamStart();
517
- const eventSourceUrl = `${backendUrl}/api/stream/${run_id}`;
518
- try {
519
- const response2 = await fetch(eventSourceUrl);
520
- const reader = response2.body?.getReader();
521
- const decoder = new TextDecoder();
522
- if (!reader) {
523
- throw new Error("No response body");
656
+ case "windows": {
657
+ const winBinaryExists = await isDockerInstalledButNotRunning();
658
+ if (winBinaryExists) {
659
+ log.step("Docker Desktop is installed but not running. Attempting to start...");
660
+ log.step("Launching Docker...");
661
+ try {
662
+ await execa("powershell", ["-Command", "Start-Process 'Docker Desktop'"], { stdio: "inherit" });
663
+ log.dim("Docker Desktop launch command sent.");
664
+ } catch {
665
+ log.warn("Could not launch Docker Desktop automatically. It may need to be started manually.");
666
+ }
667
+ log.step("Waiting for Docker...");
668
+ try {
669
+ await waitForDockerDaemon(12e4);
670
+ } catch {
671
+ throw new Error(
672
+ "Docker Desktop did not start within 120 seconds.\n\n Next steps:\n 1. Open Docker Desktop manually from the Start Menu\n 2. Wait for the whale icon to appear in the system tray\n 3. Re-run: buildwithnexus init"
673
+ );
674
+ }
675
+ return;
524
676
  }
525
- let buffer = "";
526
- while (true) {
527
- const { done, value } = await reader.read();
528
- if (done) break;
529
- buffer += decoder.decode(value, { stream: true });
530
- const lines = buffer.split("\n");
531
- buffer = lines.pop() || "";
532
- for (const line of lines) {
533
- if (line.startsWith("data: ")) {
534
- try {
535
- const data = JSON.parse(line.slice(6));
536
- const type = data.type;
537
- const content = data.data["content"] || "";
538
- if (type === "done") {
539
- tui.displayEvent(type, { content: "Task completed successfully" });
540
- tui.displayComplete(tui.getElapsedTime());
541
- process.exit(0);
542
- } else if (type === "error") {
543
- tui.displayError(content);
544
- process.exit(1);
545
- } else {
546
- tui.displayEvent(type, { content });
547
- }
548
- } catch {
549
- }
550
- }
677
+ log.step("Installing Docker Desktop...");
678
+ let installed = false;
679
+ try {
680
+ await execa("choco", ["--version"]);
681
+ log.dim("Chocolatey detected. Installing Docker Desktop via choco...");
682
+ try {
683
+ await execa("choco", ["install", "docker-desktop", "-y"], { stdio: "inherit" });
684
+ installed = true;
685
+ log.success("Docker Desktop installed via Chocolatey.");
686
+ } catch {
687
+ log.warn("Chocolatey install failed. Falling back to direct download...");
551
688
  }
689
+ } catch {
690
+ log.dim("Chocolatey not found. Using direct download...");
552
691
  }
553
- } catch (error) {
554
- console.error(
555
- "\nStream error. Make sure backend is running:\n buildwithnexus server"
556
- );
557
- process.exit(1);
692
+ if (!installed) {
693
+ log.dim("Downloading Docker Desktop installer from docker.com...");
694
+ try {
695
+ await execa("powershell", [
696
+ "-Command",
697
+ "Invoke-WebRequest -Uri 'https://desktop.docker.com/win/main/amd64/Docker Desktop Installer.exe' -OutFile '$env:TEMP\\DockerInstaller.exe'; & '$env:TEMP\\DockerInstaller.exe' Install --quiet; Remove-Item '$env:TEMP\\DockerInstaller.exe' -Force -ErrorAction SilentlyContinue"
698
+ ], { stdio: "inherit" });
699
+ installed = true;
700
+ log.success("Docker Desktop installed via direct download.");
701
+ } catch {
702
+ throw new Error(
703
+ "Failed to install Docker Desktop on Windows.\n\n Please install Docker Desktop manually:\n 1. Download from https://www.docker.com/products/docker-desktop\n 2. Run the installer and follow the prompts\n 3. Start Docker Desktop\n 4. Re-run: buildwithnexus init"
704
+ );
705
+ }
706
+ }
707
+ log.step("Launching Docker...");
708
+ try {
709
+ await execa("powershell", ["-Command", "Start-Process 'Docker Desktop'"], { stdio: "inherit" });
710
+ log.dim("Docker Desktop launch command sent.");
711
+ } catch {
712
+ log.warn("Could not launch Docker Desktop automatically after install.");
713
+ }
714
+ log.step("Waiting for Docker...");
715
+ try {
716
+ await waitForDockerDaemon(12e4);
717
+ } catch {
718
+ throw new Error(
719
+ "Docker Desktop was installed but did not start within 120 seconds.\n\n Next steps:\n 1. You may need to restart your computer for Docker to work\n 2. Open Docker Desktop from the Start Menu\n 3. Wait for the whale icon to appear in the system tray\n 4. Re-run: buildwithnexus init"
720
+ );
721
+ }
722
+ break;
558
723
  }
559
- } catch (error) {
560
- const message = error instanceof Error ? error.message : String(error);
561
- console.error("Error:", message);
562
- process.exit(1);
724
+ default:
725
+ throw new Error(`Unsupported platform: ${p.os}`);
563
726
  }
564
727
  }
728
+ async function waitForDockerDaemon(timeoutMs) {
729
+ const start = Date.now();
730
+ log.step("Waiting for Docker daemon...");
731
+ while (Date.now() - start < timeoutMs) {
732
+ try {
733
+ await execa("docker", ["info"]);
734
+ log.success("Docker daemon is ready");
735
+ return;
736
+ } catch {
737
+ }
738
+ await new Promise((r) => setTimeout(r, 3e3));
739
+ }
740
+ throw new Error(
741
+ `Docker daemon did not become ready within ${Math.round(timeoutMs / 1e3)}s.
565
742
 
566
- // src/cli/dashboard-command.ts
567
- import { spawn } from "child_process";
568
- import { fileURLToPath } from "url";
569
- import path3 from "path";
570
- var __dirname = path3.dirname(fileURLToPath(import.meta.url));
571
- async function dashboardCommand(options) {
572
- const port = options.port || "4201";
573
- console.log(`Starting Nexus Dashboard on http://localhost:${port}
574
- `);
575
- const dashboardPath = path3.join(__dirname, "../deep-agents/dashboard/server.js");
576
- const dashboard = spawn("node", [dashboardPath], {
577
- env: { ...process.env, PORT: port },
578
- stdio: "inherit"
579
- });
580
- dashboard.on("error", (err) => {
581
- console.error("Failed to start dashboard:", err);
582
- process.exit(1);
583
- });
584
- console.log(`Dashboard ready! Open: http://localhost:${port}`);
585
- console.log("Press Ctrl+C to stop\n");
743
+ Please ensure Docker is running, then re-run:
744
+ buildwithnexus init`
745
+ );
586
746
  }
587
-
588
- // src/cli/interactive.ts
589
- import * as readline2 from "readline";
590
- import chalk2 from "chalk";
591
-
592
- // src/cli/intent-classifier.ts
593
- var PLAN_KEYWORDS = [
594
- "design",
595
- "plan",
596
- "architect",
597
- "structure",
598
- "outline",
599
- "roadmap",
600
- "strategy",
601
- "organize",
602
- "breakdown",
603
- "scope",
604
- "model",
605
- "schema"
606
- ];
607
- var BUILD_KEYWORDS = [
608
- "build",
609
- "create",
610
- "make",
611
- "write",
612
- "implement",
613
- "code",
614
- "generate",
615
- "add",
616
- "fix",
617
- "update",
618
- "deploy",
619
- "run",
620
- "start",
621
- "launch",
622
- "install",
623
- "set up",
624
- "setup",
625
- "refactor",
626
- "migrate"
627
- ];
628
- var BRAINSTORM_KEYWORDS = [
629
- "what",
630
- "should",
631
- "idea",
632
- "ideas",
633
- "think",
634
- "consider",
635
- "suggest",
636
- "brainstorm",
637
- "explore",
638
- "wonder",
639
- "might",
640
- "could",
641
- "would",
642
- "how about",
643
- "what if",
644
- "options",
645
- "alternatives",
646
- "thoughts"
647
- ];
648
- function classifyIntent(task) {
649
- const lower = task.toLowerCase().trim();
650
- let planScore = 0;
651
- let buildScore = 0;
652
- let brainstormScore = 0;
653
- for (const kw of PLAN_KEYWORDS) {
654
- if (lower.includes(kw)) planScore++;
747
+ async function imageExistsLocally(image, tag) {
748
+ const ref = `${image}:${tag}`;
749
+ try {
750
+ await execa("docker", ["image", "inspect", ref]);
751
+ return true;
752
+ } catch {
753
+ return false;
655
754
  }
656
- for (const kw of BUILD_KEYWORDS) {
657
- if (lower.includes(kw)) buildScore++;
755
+ }
756
+ async function pullImage(image, tag) {
757
+ const ref = `${image}:${tag}`;
758
+ log.step(`Pulling image ${ref}...`);
759
+ try {
760
+ await execa("docker", ["pull", ref], { stdio: "inherit" });
761
+ log.success(`Image ${ref} pulled`);
762
+ } catch (err) {
763
+ log.error(`Failed to pull image ${ref}`);
764
+ throw err;
658
765
  }
659
- for (const kw of BRAINSTORM_KEYWORDS) {
660
- if (lower.includes(kw)) brainstormScore++;
766
+ }
767
+ async function startNexus(keys, config) {
768
+ log.step("Starting NEXUS container...");
769
+ try {
770
+ await execa("docker", [
771
+ "run",
772
+ "-d",
773
+ "--name",
774
+ CONTAINER_NAME,
775
+ "-e",
776
+ `ANTHROPIC_API_KEY=${keys.anthropic}`,
777
+ "-e",
778
+ `OPENAI_API_KEY=${keys.openai}`,
779
+ "-p",
780
+ `${config.port}:${config.port}`,
781
+ "buildwithnexus/nexus:latest"
782
+ ]);
783
+ log.success(`NEXUS container started on port ${config.port}`);
784
+ } catch (err) {
785
+ log.error("Failed to start NEXUS container");
786
+ throw err;
661
787
  }
662
- const wordCount = lower.split(/\s+/).length;
663
- if (wordCount > 12 && planScore === buildScore && buildScore === brainstormScore) {
664
- return "plan";
788
+ }
789
+ async function stopNexus() {
790
+ log.step("Stopping NEXUS container...");
791
+ try {
792
+ await execa("docker", ["rm", "-f", CONTAINER_NAME]);
793
+ log.success("NEXUS container stopped and removed");
794
+ } catch (err) {
795
+ log.error("Failed to stop NEXUS container");
796
+ throw err;
665
797
  }
666
- if (brainstormScore > planScore && brainstormScore > buildScore) return "brainstorm";
667
- if (buildScore > planScore && buildScore > brainstormScore) return "build";
668
- if (planScore > 0) return "plan";
669
- return wordCount > 6 ? "plan" : "build";
670
798
  }
671
-
672
- // src/cli/interactive.ts
673
- init_config();
674
- async function interactiveMode() {
675
- const backendUrl = process.env.BACKEND_URL || "http://localhost:4200";
676
- console.log(chalk2.cyan("\n\u{1F527} Configure API Keys\n"));
677
- const { deepAgentsInitCommand: deepAgentsInitCommand2 } = await Promise.resolve().then(() => (init_init_command(), init_command_exports));
678
- await deepAgentsInitCommand2();
679
- reloadEnv();
680
- if (!hasAnyKey()) {
681
- console.error("Error: At least one API key is required to use buildwithnexus.");
682
- process.exit(1);
799
+ async function dockerExec(command) {
800
+ try {
801
+ const { stdout, stderr } = await execa("docker", ["exec", CONTAINER_NAME, "sh", "-c", command]);
802
+ return { stdout, stderr, code: 0 };
803
+ } catch (err) {
804
+ const e = err;
805
+ return { stdout: e.stdout ?? "", stderr: e.stderr ?? "", code: e.exitCode ?? 1 };
683
806
  }
684
- const keys = loadApiKeys();
685
- console.log(chalk2.green("\n\u2713 Keys configured!"));
686
- console.log(chalk2.gray(` Anthropic: ${keys.anthropic ? "\u2713" : "\u2717"}`));
687
- console.log(chalk2.gray(` OpenAI: ${keys.openai ? "\u2713" : "\u2717"}`));
688
- console.log(chalk2.gray(` Google: ${keys.google ? "\u2713" : "\u2717"}
689
- `));
807
+ }
808
+ async function startBackend() {
809
+ const { spawn: spawn2 } = await import("child_process");
810
+ const os5 = await import("os");
811
+ const path11 = await import("path");
812
+ const nexusDir = path11.join(os5.homedir(), "Projects", "nexus");
813
+ log.step(`Starting Nexus backend from ${nexusDir}...`);
814
+ const child = spawn2("python", ["-m", "src.deep_agents_server"], {
815
+ cwd: nexusDir,
816
+ detached: true,
817
+ stdio: "ignore",
818
+ env: { ...process.env }
819
+ });
820
+ child.unref();
821
+ log.success("Nexus backend process started");
822
+ }
823
+ async function isNexusRunning() {
690
824
  try {
691
- const response = await fetch(`${backendUrl}/health`);
692
- if (!response.ok) {
693
- console.error(chalk2.red("\u274C Backend not running. Start it with: buildwithnexus server"));
694
- process.exit(1);
695
- }
825
+ const { stdout } = await execa("docker", [
826
+ "ps",
827
+ "--filter",
828
+ `name=^/${CONTAINER_NAME}$`,
829
+ "--format",
830
+ "{{.Names}}"
831
+ ]);
832
+ return stdout.trim() === CONTAINER_NAME;
696
833
  } catch {
697
- console.error(chalk2.red("\u274C Cannot connect to backend at " + backendUrl));
698
- process.exit(1);
834
+ return false;
699
835
  }
700
- const rl = readline2.createInterface({
701
- input: process.stdin,
702
- output: process.stdout
703
- });
704
- const ask = (question) => new Promise((resolve) => rl.question(question, resolve));
705
- console.clear();
706
- console.log(chalk2.gray("Welcome! Describe what you want the AI agents to do."));
707
- console.log(chalk2.gray('Type "exit" to quit.\n'));
708
- while (true) {
709
- const task = await ask(chalk2.bold.blue("\u{1F4DD} Task: "));
710
- if (task.toLowerCase() === "exit") {
711
- console.log(chalk2.yellow("\nGoodbye! \u{1F44B}\n"));
712
- rl.close();
713
- process.exit(0);
714
- }
715
- if (!task.trim()) {
716
- console.log(chalk2.red("Please enter a task.\n"));
717
- continue;
718
- }
719
- const suggestedMode = classifyIntent(task).toUpperCase();
720
- tui.displaySuggestedMode(suggestedMode, task);
721
- const currentMode = await selectMode(suggestedMode, ask);
722
- await runModeLoop(currentMode, task, backendUrl, rl, ask);
723
- console.log("");
836
+ }
837
+ var CONTAINER_NAME, DOCKER_DESKTOP_APP_PATH;
838
+ var init_docker = __esm({
839
+ "src/core/docker.ts"() {
840
+ "use strict";
841
+ init_logger();
842
+ init_platform();
843
+ CONTAINER_NAME = "nexus";
844
+ DOCKER_DESKTOP_APP_PATH = "/Applications/Docker.app";
724
845
  }
846
+ });
847
+
848
+ // src/ui/banner.ts
849
+ var banner_exports = {};
850
+ __export(banner_exports, {
851
+ showCompletion: () => showCompletion,
852
+ showPhase: () => showPhase,
853
+ showSecurityPosture: () => showSecurityPosture
854
+ });
855
+ import chalk5 from "chalk";
856
+ function showPhase(phase, total, description) {
857
+ const progress = chalk5.cyan(`[${phase}/${total}]`);
858
+ console.log(`
859
+ ${progress} ${chalk5.bold(description)}`);
725
860
  }
726
- async function selectMode(suggested, ask) {
727
- const modeColor = {
728
- PLAN: chalk2.cyan,
729
- BUILD: chalk2.green,
730
- BRAINSTORM: chalk2.blue
731
- };
732
- console.log("");
733
- console.log(
734
- chalk2.gray("Press ") + chalk2.bold("Enter") + chalk2.gray(" to use ") + modeColor[suggested](suggested) + chalk2.gray(" or type ") + chalk2.bold("plan") + chalk2.gray("/") + chalk2.bold("build") + chalk2.gray("/") + chalk2.bold("brainstorm") + chalk2.gray(" to switch: ")
861
+ function showSecurityPosture() {
862
+ const lines = [
863
+ "",
864
+ chalk5.bold(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"),
865
+ chalk5.bold(" \u2551 ") + chalk5.bold.green("Security Posture") + chalk5.bold(" \u2551"),
866
+ chalk5.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
867
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" Triple-nested isolation: Host \u2192 VM \u2192 Docker \u2192 KVM".padEnd(54)) + chalk5.bold("\u2551"),
868
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" Network hardened: UFW deny-all, allow 22/80/443/4200".padEnd(54)) + chalk5.bold("\u2551"),
869
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" All databases encrypted at rest (AES-256-CBC)".padEnd(54)) + chalk5.bold("\u2551"),
870
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" API keys never embedded in VM \u2014 delivered via SCP".padEnd(54)) + chalk5.bold("\u2551"),
871
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" SSH-only communication (no exposed network ports)".padEnd(54)) + chalk5.bold("\u2551"),
872
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" DLP: secret detection, shell escaping, output redaction".padEnd(54)) + chalk5.bold("\u2551"),
873
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" HMAC integrity verification on all key files".padEnd(54)) + chalk5.bold("\u2551"),
874
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" Docker: --read-only, no-new-privileges, cap-drop=ALL".padEnd(54)) + chalk5.bold("\u2551"),
875
+ chalk5.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
876
+ chalk5.bold(" \u2551 ") + chalk5.dim("Full details: https://buildwithnexus.dev/security".padEnd(55)) + chalk5.bold("\u2551"),
877
+ chalk5.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"),
878
+ ""
879
+ ];
880
+ console.log(lines.join("\n"));
881
+ }
882
+ function showCompletion(urls) {
883
+ const lines = [
884
+ "",
885
+ chalk5.green(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"),
886
+ chalk5.green(" \u2551 ") + chalk5.bold.green("NEXUS Runtime is Live!") + chalk5.green(" \u2551"),
887
+ chalk5.green(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
888
+ chalk5.green(" \u2551 ") + chalk5.white(`Connect: ${urls.ssh}`.padEnd(55)) + chalk5.green("\u2551")
889
+ ];
890
+ if (urls.remote) {
891
+ lines.push(chalk5.green(" \u2551 ") + chalk5.white(`Remote: ${urls.remote}`.padEnd(55)) + chalk5.green("\u2551"));
892
+ }
893
+ lines.push(
894
+ chalk5.green(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
895
+ chalk5.green(" \u2551 ") + chalk5.dim("Quick Start:".padEnd(55)) + chalk5.green("\u2551"),
896
+ chalk5.green(" \u2551 ") + chalk5.white(" buildwithnexus - Interactive shell".padEnd(55)) + chalk5.green("\u2551"),
897
+ chalk5.green(" \u2551 ") + chalk5.white(" buildwithnexus brainstorm - Brainstorm ideas".padEnd(55)) + chalk5.green("\u2551"),
898
+ chalk5.green(" \u2551 ") + chalk5.white(" buildwithnexus status - Check health".padEnd(55)) + chalk5.green("\u2551"),
899
+ chalk5.green(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
900
+ chalk5.green(" \u2551 ") + chalk5.dim("All commands:".padEnd(55)) + chalk5.green("\u2551"),
901
+ chalk5.green(" \u2551 ") + chalk5.white(" buildwithnexus stop/start/update/logs/ssh/destroy".padEnd(55)) + chalk5.green("\u2551"),
902
+ chalk5.green(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"),
903
+ ""
735
904
  );
736
- const answer = await ask(chalk2.gray("> "));
737
- const lower = answer.trim().toLowerCase();
738
- if (lower === "p" || lower === "plan") return "PLAN";
739
- if (lower === "b" || lower === "build") return "BUILD";
740
- if (lower === "br" || lower === "brainstorm") return "BRAINSTORM";
741
- return suggested;
905
+ console.log(lines.join("\n"));
742
906
  }
743
- async function runModeLoop(mode, task, backendUrl, rl, ask) {
744
- let currentMode = mode;
745
- while (true) {
907
+ var init_banner = __esm({
908
+ "src/ui/banner.ts"() {
909
+ "use strict";
910
+ }
911
+ });
912
+
913
+ // src/bin.ts
914
+ init_init_command();
915
+ import { program } from "commander";
916
+
917
+ // src/cli/tui.ts
918
+ import chalk from "chalk";
919
+ import stringWidth from "string-width";
920
+ var colors = {
921
+ accent: chalk.hex("#7D56F4"),
922
+ // Brand purple
923
+ success: chalk.hex("#00FF87"),
924
+ // Bright green
925
+ warning: chalk.hex("#FFB86C"),
926
+ // Amber
927
+ info: chalk.hex("#8BE9FD"),
928
+ // Cyan
929
+ muted: chalk.gray,
930
+ // Adaptive gray
931
+ error: chalk.red
932
+ };
933
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
934
+ var STATUS_SYMBOLS = {
935
+ done: colors.success("\u2714"),
936
+ active: colors.info("\u25C9"),
937
+ pending: colors.muted("\u25CB"),
938
+ error: colors.error("\u2716")
939
+ };
940
+ var TUI = class {
941
+ taskStartTime = 0;
942
+ eventCount = 0;
943
+ spinnerIndex = 0;
944
+ getSpinner() {
945
+ const frame = SPINNER_FRAMES[this.spinnerIndex % SPINNER_FRAMES.length];
946
+ this.spinnerIndex++;
947
+ return frame;
948
+ }
949
+ displayHeader(task, agent) {
746
950
  console.clear();
747
- printAppHeader();
748
- tui.displayModeBar(currentMode);
749
- tui.displayModeHeader(currentMode);
750
- if (currentMode === "PLAN") {
751
- const next = await planModeLoop(task, backendUrl, rl, ask);
752
- if (next === "BUILD") {
753
- currentMode = "BUILD";
754
- continue;
755
- }
756
- if (next === "switch") {
757
- currentMode = await promptModeSwitch(currentMode, ask);
758
- continue;
759
- }
951
+ const headerBox = this.makeRoundedBox(
952
+ colors.accent("\u{1F680} NEXUS - Autonomous Agent Orchestration"),
953
+ 62,
954
+ colors.accent
955
+ );
956
+ console.log(headerBox);
957
+ console.log("");
958
+ console.log(colors.muted("Task") + colors.muted(": ") + chalk.white(task));
959
+ console.log(colors.muted("Agent") + colors.muted(": ") + colors.info(agent));
960
+ console.log("");
961
+ this.taskStartTime = Date.now();
962
+ }
963
+ displayConnecting() {
964
+ console.log(`${this.getSpinner()} ${colors.warning("Connecting to backend...")}`);
965
+ }
966
+ displayConnected(runId) {
967
+ console.log(`${STATUS_SYMBOLS.done} Connected \u2022 ${colors.muted(`run: ${runId}`)}`);
968
+ console.log("");
969
+ }
970
+ displayStreamStart() {
971
+ console.log(chalk.bold(colors.accent("\u{1F4E1} Streaming Events")));
972
+ console.log("");
973
+ }
974
+ displayPlan(task, steps) {
975
+ console.log("");
976
+ const planHeader = colors.accent("Plan Breakdown");
977
+ const planLines = [];
978
+ steps.forEach((step, i) => {
979
+ planLines.push(` ${STATUS_SYMBOLS.pending} ${chalk.white(`Step ${i + 1}:`)} ${step}`);
980
+ });
981
+ const planBox = this.makeDoubleBox(planHeader, planLines.join("\n"), 62, colors.accent);
982
+ console.log(planBox);
983
+ console.log("");
984
+ }
985
+ displayEvent(type, data) {
986
+ this.eventCount++;
987
+ const content = data["content"] || "";
988
+ if (type === "agent_working") {
989
+ const agent = data["agent"] || "Agent";
990
+ const agentTask = data["task"] || "";
991
+ console.log("");
992
+ console.log(` ${colors.info("\u{1F464}")} ${colors.info(chalk.bold(agent))}`);
993
+ console.log(` ${colors.muted("\u2192")} ${agentTask}`);
760
994
  return;
761
995
  }
762
- if (currentMode === "BUILD") {
763
- const next = await buildModeLoop(task, backendUrl, rl, ask);
764
- if (next === "switch") {
765
- currentMode = await promptModeSwitch(currentMode, ask);
766
- continue;
996
+ if (type === "agent_result") {
997
+ const result = data["result"] || "";
998
+ let displayResult = result;
999
+ if (displayResult.length > 100) {
1000
+ displayResult = displayResult.substring(0, 97) + "...";
767
1001
  }
1002
+ console.log(` ${STATUS_SYMBOLS.done} ${chalk.white(displayResult)}`);
768
1003
  return;
769
1004
  }
770
- if (currentMode === "BRAINSTORM") {
771
- const next = await brainstormModeLoop(task, backendUrl, rl, ask);
772
- if (next === "switch") {
773
- currentMode = await promptModeSwitch(currentMode, ask);
774
- continue;
775
- }
776
- return;
1005
+ const eventConfig = {
1006
+ thought: { icon: "\u{1F4AD}", color: colors.info },
1007
+ action: { icon: "\u26A1", color: colors.warning },
1008
+ observation: { icon: "\u2713", color: colors.success },
1009
+ started: { icon: "\u25B6", color: colors.info },
1010
+ done: { icon: "\u2728", color: colors.success },
1011
+ execution_complete: { icon: "\u2728", color: colors.success },
1012
+ error: { icon: "\u2716", color: colors.error }
1013
+ };
1014
+ const config = eventConfig[type] || { icon: "\u25CF", color: colors.muted };
1015
+ let displayContent = content;
1016
+ if (displayContent.length > 100) {
1017
+ displayContent = displayContent.substring(0, 97) + "...";
777
1018
  }
1019
+ console.log(` ${config.icon} ${config.color(displayContent)}`);
778
1020
  }
779
- }
780
- function printAppHeader() {
781
- console.log(chalk2.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
782
- console.log(
783
- chalk2.cyan("\u2551") + chalk2.bold.white(" \u{1F680} Nexus - Autonomous Agent Orchestration ") + chalk2.cyan("\u2551")
784
- );
785
- console.log(chalk2.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
786
- console.log("");
787
- }
788
- async function promptModeSwitch(current, ask) {
789
- const others = ["PLAN", "BUILD", "BRAINSTORM"].filter((m) => m !== current);
790
- console.log("");
791
- console.log(
792
- chalk2.gray("Switch to: ") + others.map((m, i) => chalk2.bold(`[${i + 1}] ${m}`)).join(chalk2.gray(" ")) + chalk2.gray(" [Enter to stay in ") + chalk2.bold(current) + chalk2.gray("]")
793
- );
794
- const answer = await ask(chalk2.gray("> "));
795
- const n = parseInt(answer.trim(), 10);
796
- if (n === 1) return others[0];
797
- if (n === 2) return others[1];
798
- return current;
799
- }
800
- async function planModeLoop(task, backendUrl, rl, ask) {
801
- console.log(chalk2.bold("Task:"), chalk2.white(task));
802
- console.log("");
803
- console.log(chalk2.yellow("\u23F3 Fetching plan from backend..."));
804
- let steps = [];
805
- const keys = loadApiKeys();
806
- try {
807
- const response = await fetch(`${backendUrl}/api/run`, {
808
- method: "POST",
809
- headers: { "Content-Type": "application/json" },
810
- body: JSON.stringify({ task, agent_role: "engineer", agent_goal: "", api_key: keys.anthropic || "", openai_api_key: keys.openai || "", google_api_key: keys.google || "" })
811
- });
812
- if (!response.ok) {
813
- console.error(chalk2.red("Backend error \u2014 cannot fetch plan."));
814
- return "cancel";
815
- }
816
- const { run_id } = await response.json();
817
- tui.displayConnected(run_id);
818
- const streamResponse = await fetch(`${backendUrl}/api/stream/${run_id}`);
819
- const reader = streamResponse.body?.getReader();
820
- const decoder = new TextDecoder();
821
- if (!reader) throw new Error("No response body");
822
- let buffer = "";
823
- let planReceived = false;
824
- outer: while (true) {
825
- const { done, value } = await reader.read();
826
- if (done) break;
827
- buffer += decoder.decode(value, { stream: true });
828
- const lines = buffer.split("\n");
829
- buffer = lines.pop() || "";
830
- for (const line of lines) {
831
- if (!line.startsWith("data: ")) continue;
832
- try {
833
- const parsed = JSON.parse(line.slice(6));
834
- if (parsed.type === "plan") {
835
- steps = parsed.data["steps"] || [];
836
- planReceived = true;
837
- break outer;
838
- } else if (parsed.type === "error") {
839
- const errorMsg = parsed.data["error"] || parsed.data["content"] || "Unknown error";
840
- tui.displayError(errorMsg);
841
- return "cancel";
842
- }
843
- } catch {
844
- }
845
- }
1021
+ displayResults(summary, todosCompleted) {
1022
+ console.log("");
1023
+ console.log(colors.success("\u2501".repeat(60)));
1024
+ console.log(colors.success.bold("\u2728 Execution Complete"));
1025
+ console.log("");
1026
+ const lines = summary.split("\n");
1027
+ for (const line of lines) {
1028
+ console.log(` ${colors.success("\u2502")} ${chalk.white(line)}`);
846
1029
  }
847
- reader.cancel();
848
- if (!planReceived || steps.length === 0) {
849
- console.log(chalk2.yellow("No plan received from backend."));
850
- steps = ["(no steps returned \u2014 execute anyway?)"];
1030
+ console.log("");
1031
+ console.log(` ${colors.muted(`${todosCompleted} subtask(s) completed`)}`);
1032
+ console.log("");
1033
+ }
1034
+ displayError(error) {
1035
+ console.log("");
1036
+ console.log(colors.error("\u274C Error"));
1037
+ console.log(colors.error(error));
1038
+ console.log("");
1039
+ }
1040
+ displayComplete(duration) {
1041
+ const seconds = (duration / 1e3).toFixed(1);
1042
+ console.log("");
1043
+ console.log(colors.success.bold(`\u2728 Complete in ${seconds}s`));
1044
+ console.log(colors.muted(`${this.eventCount} event(s) streamed`));
1045
+ console.log("");
1046
+ }
1047
+ displayBox(title, content) {
1048
+ const box = this.makeRoundedBox(title, 60, colors.accent);
1049
+ console.log(box);
1050
+ const lines = content.split("\n");
1051
+ for (const line of lines) {
1052
+ const padded = line.substring(0, 56).padEnd(56);
1053
+ console.log(` ${padded}`);
851
1054
  }
852
- } catch (err) {
853
- const msg = err instanceof Error ? err.message : String(err);
854
- console.error(chalk2.red("Error: " + msg));
855
- return "cancel";
1055
+ console.log("");
856
1056
  }
857
- displayPlanSteps(steps);
858
- while (true) {
859
- console.log(chalk2.gray("Options: ") + chalk2.bold("[Y]") + chalk2.gray(" Execute ") + chalk2.bold("[e]") + chalk2.gray(" Edit step ") + chalk2.bold("[s]") + chalk2.gray(" Switch mode ") + chalk2.bold("[Esc/n]") + chalk2.gray(" Cancel"));
860
- const answer = (await ask(tui.displayPermissionPrompt("Execute this plan?"))).trim().toLowerCase();
861
- if (answer === "" || answer === "y") {
862
- return "BUILD";
1057
+ getElapsedTime() {
1058
+ return Date.now() - this.taskStartTime;
1059
+ }
1060
+ displayModeBar(current) {
1061
+ const modes = ["PLAN", "BUILD", "BRAINSTORM"];
1062
+ const modeConfig = {
1063
+ PLAN: { icon: "\u{1F4CB}", color: colors.info },
1064
+ BUILD: { icon: "\u2699\uFE0F", color: colors.success },
1065
+ BRAINSTORM: { icon: "\u{1F4A1}", color: chalk.blue }
1066
+ };
1067
+ const parts = modes.map((m) => {
1068
+ const config = modeConfig[m];
1069
+ const label = `${config.icon} ${m}`;
1070
+ if (m === current) {
1071
+ return config.color.bold(label);
1072
+ }
1073
+ return colors.muted(label);
1074
+ });
1075
+ console.log(parts.join(colors.muted(" \u2022 ")));
1076
+ console.log(colors.muted("[s] switch mode"));
1077
+ console.log("");
1078
+ }
1079
+ displayModeHeader(mode) {
1080
+ const config = {
1081
+ PLAN: {
1082
+ icon: "\u{1F4CB}",
1083
+ desc: "Break down and review steps",
1084
+ color: colors.info
1085
+ },
1086
+ BUILD: {
1087
+ icon: "\u2699\uFE0F",
1088
+ desc: "Execute with live streaming",
1089
+ color: colors.success
1090
+ },
1091
+ BRAINSTORM: {
1092
+ icon: "\u{1F4A1}",
1093
+ desc: "Free-form Q&A and exploration",
1094
+ color: chalk.blue
1095
+ }
1096
+ };
1097
+ const c = config[mode];
1098
+ console.log("");
1099
+ console.log(c.color.bold(`${c.icon} ${mode}`));
1100
+ console.log(colors.muted(c.desc));
1101
+ console.log("");
1102
+ }
1103
+ displaySuggestedMode(mode, task) {
1104
+ const modeColor = {
1105
+ PLAN: colors.info,
1106
+ BUILD: colors.success,
1107
+ BRAINSTORM: chalk.blue
1108
+ };
1109
+ const taskPreview = task.length > 45 ? task.substring(0, 42) + "..." : task;
1110
+ console.log("");
1111
+ console.log(
1112
+ colors.muted("Suggested: ") + modeColor[mode].bold(mode) + colors.muted(` for "${taskPreview}"`)
1113
+ );
1114
+ }
1115
+ displayBrainstormResponse(response) {
1116
+ console.log("");
1117
+ console.log(chalk.blue.bold("\u{1F4A1} Ideas & Analysis"));
1118
+ console.log("");
1119
+ const lines = response.split("\n");
1120
+ for (const line of lines) {
1121
+ if (line.trim()) {
1122
+ console.log(` ${chalk.white(line)}`);
1123
+ } else {
1124
+ console.log("");
1125
+ }
863
1126
  }
864
- if (answer === "n" || answer === "\x1B") {
865
- console.log(chalk2.yellow("\nExecution cancelled.\n"));
866
- return "cancel";
1127
+ console.log("");
1128
+ }
1129
+ displayPermissionPrompt(message) {
1130
+ return colors.accent.bold(message) + colors.muted(" (y/n) ");
1131
+ }
1132
+ padToWidth(text, targetWidth) {
1133
+ const visibleWidth = stringWidth(text);
1134
+ const padding = Math.max(0, targetWidth - visibleWidth);
1135
+ return text + " ".repeat(padding);
1136
+ }
1137
+ truncateToWidth(text, maxWidth) {
1138
+ let result = "";
1139
+ let width = 0;
1140
+ for (const char of text) {
1141
+ const charWidth = stringWidth(char);
1142
+ if (width + charWidth > maxWidth) break;
1143
+ result += char;
1144
+ width += charWidth;
867
1145
  }
868
- if (answer === "e" || answer === "edit") {
869
- steps = await editPlanSteps(steps, ask);
870
- displayPlanSteps(steps);
871
- continue;
1146
+ return result;
1147
+ }
1148
+ makeRoundedBox(title, width, borderColor) {
1149
+ const lines = [];
1150
+ const innerWidth = width - 4;
1151
+ const titleText = ` ${title} `;
1152
+ const titleTruncated = this.truncateToWidth(titleText, innerWidth);
1153
+ const titlePadded = this.padToWidth(titleTruncated, innerWidth);
1154
+ lines.push(borderColor("\u256D" + "\u2500".repeat(innerWidth) + "\u256E"));
1155
+ lines.push(borderColor("\u2502") + chalk.bold(titlePadded) + borderColor("\u2502"));
1156
+ lines.push(borderColor("\u2570" + "\u2500".repeat(innerWidth) + "\u256F"));
1157
+ return lines.join("\n");
1158
+ }
1159
+ makeDoubleBox(title, content, width, borderColor) {
1160
+ const lines = [];
1161
+ const innerWidth = width - 4;
1162
+ const titleText = ` ${title} `;
1163
+ const titleTruncated = this.truncateToWidth(titleText, innerWidth);
1164
+ const titlePadded = this.padToWidth(titleTruncated, innerWidth);
1165
+ lines.push(borderColor("\u2554" + "\u2550".repeat(innerWidth) + "\u2557"));
1166
+ lines.push(borderColor("\u2551") + chalk.bold(titlePadded) + borderColor("\u2551"));
1167
+ lines.push(borderColor("\u2560" + "\u2550".repeat(innerWidth) + "\u2563"));
1168
+ const contentLines = content.split("\n");
1169
+ for (const line of contentLines) {
1170
+ const contentWidth = innerWidth - 2;
1171
+ const truncated = this.truncateToWidth(line, contentWidth);
1172
+ const padded = this.padToWidth(truncated, contentWidth);
1173
+ lines.push(borderColor("\u2551") + " " + padded + borderColor("\u2551"));
872
1174
  }
873
- if (answer === "s" || answer === "switch") {
874
- return "switch";
1175
+ lines.push(borderColor("\u255A" + "\u2550".repeat(innerWidth) + "\u255D"));
1176
+ return lines.join("\n");
1177
+ }
1178
+ };
1179
+ var tui = new TUI();
1180
+
1181
+ // src/core/config.ts
1182
+ import dotenv from "dotenv";
1183
+ import path3 from "path";
1184
+ import os from "os";
1185
+ function loadApiKeys() {
1186
+ const keyNames = [
1187
+ ["ANTHROPIC_API_KEY", "anthropic"],
1188
+ ["OPENAI_API_KEY", "openai"],
1189
+ ["GOOGLE_API_KEY", "google"]
1190
+ ];
1191
+ const result = {};
1192
+ for (const [envName, key] of keyNames) {
1193
+ const raw = process.env[envName];
1194
+ if (raw !== void 0) {
1195
+ if (raw.trim() === "") {
1196
+ console.warn(`WARNING: ${envName} is set but empty - it will be treated as unconfigured`);
1197
+ result[key] = void 0;
1198
+ } else {
1199
+ result[key] = raw;
1200
+ }
1201
+ } else {
1202
+ result[key] = void 0;
875
1203
  }
876
1204
  }
1205
+ return result;
877
1206
  }
878
- function displayPlanSteps(steps) {
879
- console.log("");
880
- console.log(chalk2.bold.cyan("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
881
- console.log(chalk2.bold.cyan("\u2502") + chalk2.bold.white(" \u{1F4CB} Execution Plan ") + chalk2.bold.cyan("\u2502"));
882
- console.log(chalk2.bold.cyan("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
883
- steps.forEach((step, i) => {
884
- const label = ` Step ${i + 1}: `;
885
- const maxContentWidth = 57 - label.length;
886
- const truncated = step.length > maxContentWidth ? step.substring(0, maxContentWidth - 3) + "..." : step;
887
- const line = label + truncated;
888
- const padded = line.padEnd(57);
889
- console.log(chalk2.bold.cyan("\u2502") + chalk2.white(padded) + chalk2.bold.cyan("\u2502"));
890
- });
891
- console.log(chalk2.bold.cyan("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
892
- console.log("");
1207
+ function hasAnyKey() {
1208
+ return !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || process.env.GOOGLE_API_KEY);
893
1209
  }
894
- async function editPlanSteps(steps, ask) {
895
- console.log(chalk2.gray("Enter step number to edit, or press Enter to finish editing:"));
896
- const numStr = await ask(chalk2.bold("Step #: "));
897
- const n = parseInt(numStr.trim(), 10);
898
- if (!isNaN(n) && n >= 1 && n <= steps.length) {
899
- console.log(chalk2.gray(`Current: ${steps[n - 1]}`));
900
- const updated = await ask(chalk2.bold("New text: "));
901
- if (updated.trim()) steps[n - 1] = updated.trim();
1210
+ function validateBackendUrl(url) {
1211
+ let parsed;
1212
+ try {
1213
+ parsed = new URL(url);
1214
+ } catch {
1215
+ return { valid: false, error: `Invalid backend URL: ${url}` };
1216
+ }
1217
+ const hostname = parsed.hostname;
1218
+ const isLocal = hostname === "localhost" || hostname === "127.0.0.1";
1219
+ if (parsed.protocol === "https:") {
1220
+ return { valid: true };
1221
+ }
1222
+ if (isLocal) {
1223
+ return { valid: true };
902
1224
  }
903
- return steps;
1225
+ return {
1226
+ valid: false,
1227
+ error: "WARNING: Backend URL is not localhost and not HTTPS. API keys will be transmitted in plaintext. Use SSH tunnel or HTTPS endpoint."
1228
+ };
904
1229
  }
905
- async function buildModeLoop(task, backendUrl, rl, ask) {
906
- console.log(chalk2.bold("Task:"), chalk2.white(task));
1230
+
1231
+ // src/cli/run-command.ts
1232
+ async function runCommand(task, options) {
1233
+ const backendUrl = process.env.BACKEND_URL || "http://localhost:4200";
1234
+ const urlCheck = validateBackendUrl(backendUrl);
1235
+ if (!urlCheck.valid) {
1236
+ console.error(`
1237
+ ${urlCheck.error}`);
1238
+ process.exit(1);
1239
+ }
1240
+ tui.displayHeader(task, options.agent);
907
1241
  tui.displayConnecting();
908
- const keys = loadApiKeys();
909
1242
  try {
1243
+ let healthOk = false;
1244
+ try {
1245
+ const healthResponse = await fetch(`${backendUrl}/health`);
1246
+ healthOk = healthResponse.ok;
1247
+ } catch {
1248
+ }
1249
+ if (!healthOk) {
1250
+ console.error(
1251
+ "Backend not responding. Start it with:\n buildwithnexus server"
1252
+ );
1253
+ process.exit(1);
1254
+ }
1255
+ const keys = loadApiKeys();
910
1256
  const response = await fetch(`${backendUrl}/api/run`, {
911
1257
  method: "POST",
912
1258
  headers: { "Content-Type": "application/json" },
913
- body: JSON.stringify({ task, agent_role: "engineer", agent_goal: "", api_key: keys.anthropic || "", openai_api_key: keys.openai || "", google_api_key: keys.google || "" })
1259
+ body: JSON.stringify({
1260
+ task,
1261
+ agent_role: options.agent,
1262
+ agent_goal: options.goal || "",
1263
+ api_key: keys.anthropic || "",
1264
+ openai_api_key: keys.openai || "",
1265
+ google_api_key: keys.google || ""
1266
+ })
914
1267
  });
915
1268
  if (!response.ok) {
916
- console.error(chalk2.red("Backend error"));
917
- return "done";
1269
+ console.error("Backend error");
1270
+ console.error(await response.text());
1271
+ process.exit(1);
918
1272
  }
919
1273
  const { run_id } = await response.json();
920
1274
  tui.displayConnected(run_id);
921
- console.log(chalk2.bold.green("\u2699\uFE0F Executing..."));
922
1275
  tui.displayStreamStart();
923
- const streamResponse = await fetch(`${backendUrl}/api/stream/${run_id}`);
924
- const reader = streamResponse.body?.getReader();
925
- const decoder = new TextDecoder();
926
- if (!reader) throw new Error("No response body");
927
- let buffer = "";
928
- while (true) {
929
- const { done, value } = await reader.read();
930
- if (done) break;
931
- buffer += decoder.decode(value, { stream: true });
932
- const lines = buffer.split("\n");
933
- buffer = lines.pop() || "";
934
- for (const line of lines) {
935
- if (!line.startsWith("data: ")) continue;
936
- try {
937
- const parsed = JSON.parse(line.slice(6));
938
- const type = parsed.type;
939
- if (type === "execution_complete") {
940
- const summary = parsed.data["summary"] || "";
941
- const count = parsed.data["todos_completed"] || 0;
942
- tui.displayResults(summary, count);
943
- tui.displayComplete(tui.getElapsedTime());
944
- break;
945
- } else if (type === "done") {
946
- tui.displayEvent(type, { content: "Task completed successfully" });
947
- tui.displayComplete(tui.getElapsedTime());
948
- break;
949
- } else if (type === "error") {
950
- const errorMsg = parsed.data["error"] || parsed.data["content"] || "Unknown error";
951
- tui.displayError(errorMsg);
952
- break;
953
- } else if (type !== "plan") {
954
- tui.displayEvent(type, parsed.data);
955
- }
956
- } catch {
957
- }
958
- }
959
- }
960
- } catch (err) {
961
- const msg = err instanceof Error ? err.message : String(err);
962
- console.error(chalk2.red("Error: " + msg));
963
- }
964
- console.log("");
965
- console.log(
966
- chalk2.gray("Options: ") + chalk2.bold("[Enter]") + chalk2.gray(" Done ") + chalk2.bold("[s]") + chalk2.gray(" Switch mode")
967
- );
968
- const answer = (await ask(chalk2.bold("> "))).trim().toLowerCase();
969
- if (answer === "s" || answer === "switch") return "switch";
970
- return "done";
971
- }
972
- async function brainstormModeLoop(task, backendUrl, rl, ask) {
973
- console.log(chalk2.bold("Starting topic:"), chalk2.white(task));
974
- console.log(chalk2.gray('Ask follow-up questions. Type "done" to exit, "switch" to change mode.\n'));
975
- let currentQuestion = task;
976
- while (true) {
977
- console.log(chalk2.bold.blue("\u{1F4A1} Thinking..."));
1276
+ const eventSourceUrl = `${backendUrl}/api/stream/${run_id}`;
978
1277
  try {
979
- const keys = loadApiKeys();
980
- const response = await fetch(`${backendUrl}/api/run`, {
981
- method: "POST",
982
- headers: { "Content-Type": "application/json" },
983
- body: JSON.stringify({
984
- task: currentQuestion,
985
- agent_role: "brainstorm",
986
- agent_goal: "Generate ideas, considerations, and suggestions. Be concise and helpful.",
987
- api_key: keys.anthropic || "",
988
- openai_api_key: keys.openai || "",
989
- google_api_key: keys.google || ""
990
- })
991
- });
992
- if (response.ok) {
993
- const { run_id } = await response.json();
994
- const streamResponse = await fetch(`${backendUrl}/api/stream/${run_id}`);
995
- const reader = streamResponse.body?.getReader();
996
- const decoder = new TextDecoder();
997
- if (reader) {
998
- let buffer = "";
999
- let responseText = "";
1000
- outer: while (true) {
1001
- const { done, value } = await reader.read();
1002
- if (done) break;
1003
- buffer += decoder.decode(value, { stream: true });
1004
- const lines = buffer.split("\n");
1005
- buffer = lines.pop() || "";
1006
- for (const line of lines) {
1007
- if (!line.startsWith("data: ")) continue;
1008
- try {
1009
- const parsed = JSON.parse(line.slice(6));
1010
- const type = parsed.type;
1011
- const data = parsed.data;
1012
- if (type === "done" || type === "execution_complete") {
1013
- const summary = data["summary"] || "";
1014
- if (summary) responseText = summary;
1015
- break outer;
1016
- } else if (type === "error") {
1017
- const errorMsg = data["error"] || data["content"] || "Unknown error";
1018
- responseText += errorMsg + "\n";
1019
- break outer;
1020
- } else if (type === "thought" || type === "observation") {
1021
- const content = data["content"] || "";
1022
- if (content) responseText += content + "\n";
1023
- } else if (type === "agent_response" || type === "agent_result") {
1024
- const content = data["content"] || data["result"] || "";
1025
- if (content) responseText += content + "\n";
1026
- } else if (type === "action") {
1027
- const content = data["content"] || "";
1028
- if (content) responseText += content + "\n";
1029
- } else if (type === "agent_working") {
1030
- } else if (type !== "plan") {
1031
- const content = data["content"] || data["response"] || "";
1032
- if (content) responseText += content + "\n";
1033
- }
1034
- } catch {
1035
- }
1036
- }
1037
- }
1038
- reader.cancel();
1039
- if (responseText.trim()) {
1040
- tui.displayBrainstormResponse(responseText.trim());
1041
- } else {
1042
- console.log(chalk2.gray("(No response received from agent)"));
1043
- }
1278
+ const response2 = await fetch(eventSourceUrl);
1279
+ const reader = response2.body?.getReader();
1280
+ if (!reader) {
1281
+ throw new Error("No response body");
1282
+ }
1283
+ for await (const parsed of parseSSEStream(reader)) {
1284
+ const type = parsed.type;
1285
+ const eventContent = parsed.data["content"] || "";
1286
+ if (type === "done") {
1287
+ tui.displayEvent(type, { content: "Task completed successfully" });
1288
+ tui.displayComplete(tui.getElapsedTime());
1289
+ process.exit(0);
1290
+ } else if (type === "error") {
1291
+ tui.displayError(eventContent);
1292
+ process.exit(1);
1293
+ } else {
1294
+ tui.displayEvent(type, { content: eventContent });
1044
1295
  }
1045
- } else {
1046
- console.log(chalk2.red("Could not reach backend for brainstorm response."));
1047
1296
  }
1048
- } catch (err) {
1049
- const msg = err instanceof Error ? err.message : String(err);
1050
- console.error(chalk2.red("Error: " + msg));
1297
+ } catch (error) {
1298
+ console.error(
1299
+ "\nStream error. Make sure backend is running:\n buildwithnexus server"
1300
+ );
1301
+ process.exit(1);
1051
1302
  }
1052
- const followUp = await ask(chalk2.bold.blue("\u{1F4AC} You: "));
1053
- const lower = followUp.trim().toLowerCase();
1054
- if (lower === "done" || lower === "exit") return "done";
1055
- if (lower === "switch") return "switch";
1056
- if (!followUp.trim()) continue;
1057
- currentQuestion = followUp.trim();
1303
+ } catch (error) {
1304
+ const message = error instanceof Error ? error.message : String(error);
1305
+ console.error("Error:", message);
1306
+ process.exit(1);
1058
1307
  }
1059
1308
  }
1060
1309
 
1061
- // src/cli.ts
1062
- import { Command as Command15 } from "commander";
1063
- import { readFileSync as readFileSync2 } from "fs";
1064
- import { dirname as dirname2, join as join2 } from "path";
1065
- import { fileURLToPath as fileURLToPath4 } from "url";
1066
-
1067
- // src/commands/install.ts
1068
- import { Command } from "commander";
1069
- import { execa as execa2 } from "execa";
1070
-
1071
- // src/ui/spinner.ts
1072
- import ora from "ora";
1073
- import chalk3 from "chalk";
1074
- function createSpinner(text) {
1075
- return ora({ text, color: "cyan", spinner: "dots" });
1076
- }
1077
- function succeed(spinner, text) {
1078
- spinner.succeed(chalk3.green(text));
1079
- }
1080
- function fail(spinner, text) {
1081
- spinner.fail(chalk3.red(text));
1310
+ // src/cli/dashboard-command.ts
1311
+ import { spawn } from "child_process";
1312
+ import { fileURLToPath } from "url";
1313
+ import path4 from "path";
1314
+ var __dirname = path4.dirname(fileURLToPath(import.meta.url));
1315
+ async function dashboardCommand(options) {
1316
+ const port = options.port || "4201";
1317
+ console.log(`Starting Nexus Dashboard on http://localhost:${port}
1318
+ `);
1319
+ const dashboardPath = path4.join(__dirname, "../deep-agents/dashboard/server.js");
1320
+ const dashboard = spawn("node", [dashboardPath], {
1321
+ env: { ...process.env, PORT: port },
1322
+ stdio: "inherit"
1323
+ });
1324
+ dashboard.on("error", (err) => {
1325
+ console.error("Failed to start dashboard:", err);
1326
+ process.exit(1);
1327
+ });
1328
+ console.log(`Dashboard ready! Open: http://localhost:${port}`);
1329
+ console.log("Press Ctrl+C to stop\n");
1082
1330
  }
1083
1331
 
1084
- // src/ui/logger.ts
1085
- import chalk4 from "chalk";
1086
- var log = {
1087
- step(msg) {
1088
- console.log(chalk4.cyan(" \u2192 ") + msg);
1089
- },
1090
- success(msg) {
1091
- console.log(chalk4.green(" \u2713 ") + msg);
1092
- },
1093
- error(msg) {
1094
- console.error(chalk4.red(" \u2717 ") + msg);
1095
- },
1096
- warn(msg) {
1097
- console.log(chalk4.yellow(" \u26A0 ") + msg);
1098
- },
1099
- dim(msg) {
1100
- console.log(chalk4.dim(" " + msg));
1101
- },
1102
- detail(label, value) {
1103
- console.log(chalk4.dim(" " + label + ": ") + value);
1104
- },
1105
- progress(current, total, label) {
1106
- const pct = Math.round(current / total * 100);
1107
- const filled = Math.round(current / total * 20);
1108
- const bar = chalk4.cyan("\u2588".repeat(filled)) + chalk4.dim("\u2591".repeat(20 - filled));
1109
- process.stdout.write(`\r [${bar}] ${chalk4.bold(`${pct}%`)} ${chalk4.dim(label)}`);
1110
- if (current >= total) process.stdout.write("\n");
1111
- }
1112
- };
1113
-
1114
- // src/core/platform.ts
1115
- import os3 from "os";
1116
- function detectPlatform() {
1117
- const platform = os3.platform();
1118
- const arch = os3.arch();
1119
- if (platform === "darwin") {
1120
- return {
1121
- os: "mac",
1122
- arch: arch === "arm64" ? "arm64" : "x64",
1123
- dockerPlatform: arch === "arm64" ? "linux/arm64" : "linux/amd64"
1124
- };
1125
- }
1126
- if (platform === "linux") {
1127
- return {
1128
- os: "linux",
1129
- arch: arch === "arm64" ? "arm64" : "x64",
1130
- dockerPlatform: arch === "arm64" ? "linux/arm64" : "linux/amd64"
1131
- };
1132
- }
1133
- if (platform === "win32") {
1134
- return {
1135
- os: "windows",
1136
- arch: "x64",
1137
- dockerPlatform: "linux/amd64"
1138
- };
1139
- }
1140
- throw new Error(`Unsupported platform: ${platform} ${arch}`);
1141
- }
1332
+ // src/cli/interactive.ts
1333
+ import * as readline2 from "readline";
1334
+ import chalk3 from "chalk";
1142
1335
 
1143
- // src/core/docker.ts
1144
- import { existsSync } from "fs";
1145
- import { execa } from "execa";
1146
- var CONTAINER_NAME = "nexus";
1147
- var DOCKER_DESKTOP_APP_PATH = "/Applications/Docker.app";
1148
- async function isDockerInstalled() {
1149
- try {
1150
- await execa("docker", ["info"]);
1151
- return true;
1152
- } catch {
1153
- return false;
1154
- }
1155
- }
1156
- async function isDockerInstalledButNotRunning() {
1157
- try {
1158
- await execa("docker", ["--version"]);
1159
- return true;
1160
- } catch {
1161
- return false;
1162
- }
1163
- }
1164
- function dockerDesktopExists() {
1165
- return existsSync(DOCKER_DESKTOP_APP_PATH);
1166
- }
1167
- async function ensureHomebrew() {
1168
- try {
1169
- await execa("which", ["brew"]);
1170
- log.dim("Homebrew is already installed.");
1171
- return;
1172
- } catch {
1336
+ // src/cli/intent-classifier.ts
1337
+ var PLAN_KEYWORDS = [
1338
+ "design",
1339
+ "plan",
1340
+ "architect",
1341
+ "structure",
1342
+ "outline",
1343
+ "roadmap",
1344
+ "strategy",
1345
+ "organize",
1346
+ "breakdown",
1347
+ "scope",
1348
+ "model",
1349
+ "schema"
1350
+ ];
1351
+ var BUILD_KEYWORDS = [
1352
+ "build",
1353
+ "create",
1354
+ "make",
1355
+ "write",
1356
+ "implement",
1357
+ "code",
1358
+ "generate",
1359
+ "add",
1360
+ "fix",
1361
+ "update",
1362
+ "deploy",
1363
+ "run",
1364
+ "start",
1365
+ "launch",
1366
+ "install",
1367
+ "set up",
1368
+ "setup",
1369
+ "refactor",
1370
+ "migrate"
1371
+ ];
1372
+ var BRAINSTORM_KEYWORDS = [
1373
+ "what",
1374
+ "should",
1375
+ "idea",
1376
+ "ideas",
1377
+ "think",
1378
+ "consider",
1379
+ "suggest",
1380
+ "brainstorm",
1381
+ "explore",
1382
+ "wonder",
1383
+ "might",
1384
+ "could",
1385
+ "would",
1386
+ "how about",
1387
+ "what if",
1388
+ "options",
1389
+ "alternatives",
1390
+ "thoughts"
1391
+ ];
1392
+ function classifyIntent(task) {
1393
+ const lower = task.toLowerCase().trim();
1394
+ let planScore = 0;
1395
+ let buildScore = 0;
1396
+ let brainstormScore = 0;
1397
+ for (const kw of PLAN_KEYWORDS) {
1398
+ if (lower.includes(kw)) planScore++;
1173
1399
  }
1174
- log.step("Installing Homebrew...");
1175
- try {
1176
- await execa("/bin/bash", [
1177
- "-c",
1178
- "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
1179
- ], {
1180
- stdio: "inherit",
1181
- env: { ...process.env, NONINTERACTIVE: "1" }
1182
- });
1183
- } catch {
1184
- throw new Error(
1185
- 'Failed to install Homebrew automatically.\n\n Install Homebrew manually:\n /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"\n\n Then re-run:\n buildwithnexus init'
1186
- );
1400
+ for (const kw of BUILD_KEYWORDS) {
1401
+ if (lower.includes(kw)) buildScore++;
1187
1402
  }
1188
- try {
1189
- await execa("which", ["brew"]);
1190
- } catch {
1191
- const armPath = "/opt/homebrew/bin";
1192
- const intelPath = "/usr/local/bin";
1193
- process.env.PATH = `${armPath}:${intelPath}:${process.env.PATH}`;
1194
- log.dim("Added Homebrew paths to PATH for this session.");
1403
+ for (const kw of BRAINSTORM_KEYWORDS) {
1404
+ if (lower.includes(kw)) brainstormScore++;
1195
1405
  }
1196
- try {
1197
- await execa("brew", ["--version"]);
1198
- log.success("Homebrew installed successfully.");
1199
- } catch {
1200
- throw new Error(
1201
- "Homebrew was installed but is not available on PATH.\n\n Try opening a new terminal and re-running:\n buildwithnexus init"
1202
- );
1406
+ const wordCount = lower.split(/\s+/).length;
1407
+ if (wordCount > 12 && planScore === buildScore && buildScore === brainstormScore) {
1408
+ return "plan";
1203
1409
  }
1410
+ if (brainstormScore > planScore && brainstormScore > buildScore) return "brainstorm";
1411
+ if (buildScore > planScore && buildScore > brainstormScore) return "build";
1412
+ if (planScore > 0) return "plan";
1413
+ return wordCount > 6 ? "plan" : "build";
1204
1414
  }
1205
- async function installDocker(platform) {
1206
- const p = platform ?? detectPlatform();
1207
- switch (p.os) {
1208
- case "mac": {
1209
- if (await isDockerInstalled()) {
1210
- log.success("Docker is already running.");
1211
- return;
1212
- }
1213
- if (dockerDesktopExists()) {
1214
- log.step(`Docker Desktop found at ${DOCKER_DESKTOP_APP_PATH} but not running. Attempting to start...`);
1215
- let launched = false;
1216
- log.dim(`Trying: open ${DOCKER_DESKTOP_APP_PATH}`);
1217
- try {
1218
- await execa("open", [DOCKER_DESKTOP_APP_PATH]);
1219
- launched = true;
1220
- log.dim(`Launch command succeeded via ${DOCKER_DESKTOP_APP_PATH}`);
1221
- } catch {
1222
- log.warn(`Could not launch via ${DOCKER_DESKTOP_APP_PATH} \u2014 trying fallback...`);
1223
- }
1224
- if (!launched) {
1225
- log.dim("Trying: open -a Docker");
1226
- try {
1227
- await execa("open", ["-a", "Docker"]);
1228
- launched = true;
1229
- log.dim("Launch command succeeded via open -a Docker");
1230
- } catch {
1231
- log.warn("Both launch attempts failed.");
1232
- }
1233
- }
1234
- if (launched) {
1235
- log.step("Docker Desktop is starting up. Waiting for the daemon to be ready (up to 120s)...");
1236
- try {
1237
- await waitForDockerDaemon(12e4);
1238
- return;
1239
- } catch {
1240
- log.warn("Docker Desktop was launched but the daemon did not become ready in time.");
1241
- }
1242
- } else {
1243
- log.warn("Could not launch Docker Desktop. Will fall back to reinstalling via Homebrew.");
1244
- }
1245
- } else {
1246
- log.step(`Docker Desktop not found at ${DOCKER_DESKTOP_APP_PATH}.`);
1247
- }
1248
- log.step("Installing Docker Desktop via Homebrew...");
1249
- await ensureHomebrew();
1250
- try {
1251
- await execa("brew", ["install", "--cask", "docker"], {
1252
- stdio: "inherit",
1253
- timeout: 6e4
1254
- });
1255
- } catch (err) {
1256
- const e = err;
1257
- if (e.killed && e.signal === "SIGINT") {
1258
- throw new Error("Docker installation cancelled by user (Ctrl+C)");
1259
- }
1260
- if (e.timedOut) {
1261
- throw new Error(
1262
- "Docker installation via Homebrew timed out after 60 seconds.\n\n The password prompt may be waiting for input. Try installing manually:\n brew install --cask docker\n\n After installing, re-run:\n buildwithnexus init"
1263
- );
1264
- }
1265
- throw new Error(
1266
- "Failed to install Docker via Homebrew.\n\n Try installing Docker Desktop manually:\n https://www.docker.com/products/docker-desktop\n\n After installing, re-run:\n buildwithnexus init"
1267
- );
1268
- }
1269
- log.step("Launching Docker Desktop...");
1270
- let postInstallLaunched = false;
1271
- log.dim(`Trying: open ${DOCKER_DESKTOP_APP_PATH}`);
1272
- try {
1273
- await execa("open", [DOCKER_DESKTOP_APP_PATH]);
1274
- postInstallLaunched = true;
1275
- log.dim(`Launch command succeeded via ${DOCKER_DESKTOP_APP_PATH}`);
1276
- } catch {
1277
- log.warn(`Could not launch via ${DOCKER_DESKTOP_APP_PATH} \u2014 trying fallback...`);
1278
- }
1279
- if (!postInstallLaunched) {
1280
- log.dim("Trying: open -a Docker");
1281
- try {
1282
- await execa("open", ["-a", "Docker"]);
1283
- postInstallLaunched = true;
1284
- log.dim("Launch command succeeded via open -a Docker");
1285
- } catch {
1286
- log.warn("Both launch attempts failed after install.");
1287
- }
1288
- }
1289
- if (!postInstallLaunched) {
1290
- throw new Error(
1291
- "Docker Desktop was installed but could not be started automatically.\n\n Next steps:\n 1. Open Docker Desktop manually from your Applications folder\n 2. Wait for the whale icon to appear in the menu bar\n 3. Re-run: buildwithnexus init"
1292
- );
1293
- }
1294
- log.step("Docker Desktop is starting up. Waiting for the daemon to be ready (up to 120s)...");
1295
- await waitForDockerDaemon(12e4);
1296
- break;
1297
- }
1298
- case "linux": {
1299
- const linuxBinaryExists = await isDockerInstalledButNotRunning();
1300
- if (linuxBinaryExists) {
1301
- log.step("Docker is installed but the daemon is not running.");
1302
- log.step("Starting Docker daemon...");
1303
- try {
1304
- await execa("sudo", ["systemctl", "start", "docker"], { stdio: "inherit" });
1305
- log.dim("Started Docker daemon via systemctl.");
1306
- } catch {
1307
- try {
1308
- await execa("sudo", ["service", "docker", "start"], { stdio: "inherit" });
1309
- log.dim("Started Docker daemon via service command.");
1310
- } catch {
1311
- throw new Error(
1312
- "Docker is installed but the daemon could not be started.\n\n Try starting it manually:\n sudo systemctl start docker\n\n Then re-run:\n buildwithnexus init"
1313
- );
1314
- }
1315
- }
1316
- log.step("Waiting for Docker...");
1317
- await waitForDockerDaemon(3e4);
1318
- return;
1319
- }
1320
- log.step("Installing Docker...");
1321
- log.warn("This may require your sudo password.");
1322
- log.dim("Running official Docker install script from https://get.docker.com ...");
1323
- try {
1324
- const { stdout: script } = await execa("curl", ["-fsSL", "https://get.docker.com"]);
1325
- await execa("sudo", ["sh", "-c", script], { stdio: "inherit" });
1326
- log.success("Docker installed successfully.");
1327
- } catch {
1328
- throw new Error(
1329
- "Failed to install Docker on Linux.\n\n Try installing manually:\n curl -fsSL https://get.docker.com | sudo sh\n\n After installing, re-run:\n buildwithnexus init"
1330
- );
1331
- }
1332
- log.dim("Adding current user to docker group...");
1333
- try {
1334
- const user = (await execa("whoami")).stdout.trim();
1335
- await execa("sudo", ["usermod", "-aG", "docker", user]);
1336
- log.dim(`Added user '${user}' to docker group (may require re-login for effect).`);
1337
- } catch {
1338
- log.warn("Could not add user to docker group. You may need sudo for docker commands.");
1339
- }
1340
- log.step("Starting Docker daemon...");
1415
+
1416
+ // src/cli/interactive.ts
1417
+ init_secrets();
1418
+
1419
+ // src/core/sse-parser.ts
1420
+ async function* parseSSEStream2(reader) {
1421
+ const decoder = new TextDecoder();
1422
+ let buffer = "";
1423
+ while (true) {
1424
+ const { done, value } = await reader.read();
1425
+ if (done) break;
1426
+ buffer += decoder.decode(value, { stream: true });
1427
+ const lines = buffer.split("\n");
1428
+ buffer = lines.pop() || "";
1429
+ for (const line of lines) {
1430
+ if (!line.startsWith("data: ")) continue;
1341
1431
  try {
1342
- await execa("sudo", ["systemctl", "start", "docker"], { stdio: "inherit" });
1343
- log.dim("Started Docker daemon via systemctl.");
1344
- } catch {
1345
- try {
1346
- await execa("sudo", ["service", "docker", "start"], { stdio: "inherit" });
1347
- log.dim("Started Docker daemon via service command.");
1348
- } catch {
1349
- throw new Error(
1350
- "Docker was installed but the daemon could not be started.\n\n Try starting it manually:\n sudo systemctl start docker\n\n Then re-run:\n buildwithnexus init"
1351
- );
1352
- }
1432
+ const parsed = JSON.parse(line.slice(6));
1433
+ yield parsed;
1434
+ } catch (e) {
1435
+ if (process.env.LOG_LEVEL === "debug") console.error("SSE parse error:", e);
1353
1436
  }
1354
- log.step("Waiting for Docker...");
1355
- await waitForDockerDaemon(3e4);
1356
- break;
1357
1437
  }
1358
- case "windows": {
1359
- const winBinaryExists = await isDockerInstalledButNotRunning();
1360
- if (winBinaryExists) {
1361
- log.step("Docker Desktop is installed but not running. Attempting to start...");
1362
- log.step("Launching Docker...");
1363
- try {
1364
- await execa("powershell", ["-Command", "Start-Process 'Docker Desktop'"], { stdio: "inherit" });
1365
- log.dim("Docker Desktop launch command sent.");
1366
- } catch {
1367
- log.warn("Could not launch Docker Desktop automatically. It may need to be started manually.");
1368
- }
1369
- log.step("Waiting for Docker...");
1370
- try {
1371
- await waitForDockerDaemon(12e4);
1372
- } catch {
1373
- throw new Error(
1374
- "Docker Desktop did not start within 120 seconds.\n\n Next steps:\n 1. Open Docker Desktop manually from the Start Menu\n 2. Wait for the whale icon to appear in the system tray\n 3. Re-run: buildwithnexus init"
1375
- );
1376
- }
1377
- return;
1378
- }
1379
- log.step("Installing Docker Desktop...");
1380
- let installed = false;
1381
- try {
1382
- await execa("choco", ["--version"]);
1383
- log.dim("Chocolatey detected. Installing Docker Desktop via choco...");
1384
- try {
1385
- await execa("choco", ["install", "docker-desktop", "-y"], { stdio: "inherit" });
1386
- installed = true;
1387
- log.success("Docker Desktop installed via Chocolatey.");
1388
- } catch {
1389
- log.warn("Chocolatey install failed. Falling back to direct download...");
1390
- }
1391
- } catch {
1392
- log.dim("Chocolatey not found. Using direct download...");
1393
- }
1394
- if (!installed) {
1395
- log.dim("Downloading Docker Desktop installer from docker.com...");
1396
- try {
1397
- await execa("powershell", [
1398
- "-Command",
1399
- "Invoke-WebRequest -Uri 'https://desktop.docker.com/win/main/amd64/Docker Desktop Installer.exe' -OutFile '$env:TEMP\\DockerInstaller.exe'; & '$env:TEMP\\DockerInstaller.exe' Install --quiet; Remove-Item '$env:TEMP\\DockerInstaller.exe' -Force -ErrorAction SilentlyContinue"
1400
- ], { stdio: "inherit" });
1401
- installed = true;
1402
- log.success("Docker Desktop installed via direct download.");
1403
- } catch {
1404
- throw new Error(
1405
- "Failed to install Docker Desktop on Windows.\n\n Please install Docker Desktop manually:\n 1. Download from https://www.docker.com/products/docker-desktop\n 2. Run the installer and follow the prompts\n 3. Start Docker Desktop\n 4. Re-run: buildwithnexus init"
1406
- );
1407
- }
1438
+ }
1439
+ }
1440
+
1441
+ // src/cli/interactive.ts
1442
+ init_docker();
1443
+ var appVersion = true ? "0.7.2" : pkg.version;
1444
+ async function interactiveMode() {
1445
+ const backendUrl = process.env.BACKEND_URL || "http://localhost:4200";
1446
+ const urlCheck = validateBackendUrl(backendUrl);
1447
+ if (!urlCheck.valid) {
1448
+ console.error(chalk3.red(`
1449
+ ${urlCheck.error}`));
1450
+ process.exit(1);
1451
+ }
1452
+ if (!hasAnyKey()) {
1453
+ try {
1454
+ const storedKeys = loadKeys();
1455
+ if (storedKeys) {
1456
+ if (storedKeys.ANTHROPIC_API_KEY) process.env.ANTHROPIC_API_KEY = storedKeys.ANTHROPIC_API_KEY;
1457
+ if (storedKeys.OPENAI_API_KEY) process.env.OPENAI_API_KEY = storedKeys.OPENAI_API_KEY;
1458
+ if (storedKeys.GOOGLE_API_KEY) process.env.GOOGLE_API_KEY = storedKeys.GOOGLE_API_KEY;
1408
1459
  }
1409
- log.step("Launching Docker...");
1460
+ } catch {
1461
+ }
1462
+ }
1463
+ if (!hasAnyKey()) {
1464
+ console.log(chalk3.cyan("\n\u{1F527} Configure API Keys\n"));
1465
+ const { deepAgentsInitCommand: deepAgentsInitCommand2 } = await Promise.resolve().then(() => (init_init_command(), init_command_exports));
1466
+ await deepAgentsInitCommand2();
1467
+ if (!hasAnyKey()) {
1468
+ console.error("Error: At least one API key is required to use buildwithnexus.");
1469
+ process.exit(1);
1470
+ }
1471
+ }
1472
+ const keys = loadApiKeys();
1473
+ console.log(chalk3.green("\n\u2713 Keys configured!"));
1474
+ console.log(chalk3.gray(` Anthropic: ${keys.anthropic ? "\u2713" : "\u2717"}`));
1475
+ console.log(chalk3.gray(` OpenAI: ${keys.openai ? "\u2713" : "\u2717"}`));
1476
+ console.log(chalk3.gray(` Google: ${keys.google ? "\u2713" : "\u2717"}`));
1477
+ console.log(chalk3.gray(` (Run 'da-init' to reconfigure)
1478
+ `));
1479
+ async function waitForBackend() {
1480
+ for (let i = 0; i < 5; i++) {
1481
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
1410
1482
  try {
1411
- await execa("powershell", ["-Command", "Start-Process 'Docker Desktop'"], { stdio: "inherit" });
1412
- log.dim("Docker Desktop launch command sent.");
1483
+ const retryResponse = await fetch(`${backendUrl}/health`, { signal: AbortSignal.timeout(1e4) });
1484
+ if (retryResponse.ok) return true;
1413
1485
  } catch {
1414
- log.warn("Could not launch Docker Desktop automatically after install.");
1415
1486
  }
1416
- log.step("Waiting for Docker...");
1417
- try {
1418
- await waitForDockerDaemon(12e4);
1419
- } catch {
1420
- throw new Error(
1421
- "Docker Desktop was installed but did not start within 120 seconds.\n\n Next steps:\n 1. You may need to restart your computer for Docker to work\n 2. Open Docker Desktop from the Start Menu\n 3. Wait for the whale icon to appear in the system tray\n 4. Re-run: buildwithnexus init"
1422
- );
1487
+ }
1488
+ return false;
1489
+ }
1490
+ try {
1491
+ const response = await fetch(`${backendUrl}/health`, { signal: AbortSignal.timeout(1e4) });
1492
+ if (!response.ok) {
1493
+ console.log(chalk3.yellow("\u26A0\uFE0F Backend not responding, starting..."));
1494
+ await startBackend();
1495
+ const ready = await waitForBackend();
1496
+ if (!ready) {
1497
+ console.error(chalk3.red("\u274C Backend failed to start. Run: buildwithnexus server"));
1498
+ process.exit(1);
1423
1499
  }
1424
- break;
1425
1500
  }
1426
- default:
1427
- throw new Error(`Unsupported platform: ${p.os}`);
1501
+ } catch {
1502
+ console.log(chalk3.yellow("\u26A0\uFE0F Backend not accessible, attempting to start..."));
1503
+ await startBackend();
1504
+ const ready = await waitForBackend();
1505
+ if (!ready) {
1506
+ console.error(chalk3.red("\u274C Backend failed to start. Run: buildwithnexus server"));
1507
+ process.exit(1);
1508
+ }
1509
+ }
1510
+ const rl = readline2.createInterface({
1511
+ input: process.stdin,
1512
+ output: process.stdout
1513
+ });
1514
+ const ask = (question) => new Promise((resolve) => rl.question(question, resolve));
1515
+ console.clear();
1516
+ console.log(chalk3.gray("Welcome! Describe what you want the AI agents to do."));
1517
+ console.log(chalk3.gray('Type "exit" to quit.\n'));
1518
+ while (true) {
1519
+ const task = await ask(chalk3.bold.blue("\u{1F4DD} Task: "));
1520
+ if (task.toLowerCase() === "exit") {
1521
+ console.log(chalk3.yellow("\nGoodbye! \u{1F44B}\n"));
1522
+ rl.close();
1523
+ process.exit(0);
1524
+ }
1525
+ if (!task.trim()) {
1526
+ console.log(chalk3.red("Please enter a task.\n"));
1527
+ continue;
1528
+ }
1529
+ const suggestedMode = classifyIntent(task).toUpperCase();
1530
+ tui.displaySuggestedMode(suggestedMode, task);
1531
+ const currentMode = await selectMode(suggestedMode, ask);
1532
+ await runModeLoop(currentMode, task, backendUrl, ask);
1533
+ console.log("");
1428
1534
  }
1429
1535
  }
1430
- async function waitForDockerDaemon(timeoutMs) {
1431
- const start = Date.now();
1432
- log.step("Waiting for Docker daemon...");
1433
- while (Date.now() - start < timeoutMs) {
1434
- try {
1435
- await execa("docker", ["info"]);
1436
- log.success("Docker daemon is ready");
1536
+ async function selectMode(suggested, ask) {
1537
+ const modeColor = {
1538
+ PLAN: chalk3.cyan,
1539
+ BUILD: chalk3.green,
1540
+ BRAINSTORM: chalk3.blue
1541
+ };
1542
+ console.log("");
1543
+ console.log(
1544
+ chalk3.gray("Press ") + chalk3.bold("Enter") + chalk3.gray(" to use ") + modeColor[suggested](suggested) + chalk3.gray(" or choose a mode:")
1545
+ );
1546
+ console.log(
1547
+ chalk3.gray(" ") + chalk3.cyan.bold("[1] PLAN") + chalk3.gray(" (p) design & break down steps") + chalk3.gray("\n ") + chalk3.green.bold("[2] BUILD") + chalk3.gray(" (b) execute with live streaming") + chalk3.gray("\n ") + chalk3.blue.bold("[3] BRAINSTORM") + chalk3.gray(" (bs) free-form explore & Q&A")
1548
+ );
1549
+ const answer = await ask(chalk3.gray("> "));
1550
+ const lower = answer.trim().toLowerCase();
1551
+ if (lower === "1" || lower === "p" || lower === "plan") return "PLAN";
1552
+ if (lower === "2" || lower === "b" || lower === "build") return "BUILD";
1553
+ if (lower === "3" || lower === "bs" || lower === "br" || lower === "brainstorm") return "BRAINSTORM";
1554
+ return suggested;
1555
+ }
1556
+ async function runModeLoop(mode, task, backendUrl, ask) {
1557
+ let currentMode = mode;
1558
+ while (true) {
1559
+ console.clear();
1560
+ printAppHeader();
1561
+ tui.displayModeBar(currentMode);
1562
+ tui.displayModeHeader(currentMode);
1563
+ if (currentMode === "PLAN") {
1564
+ const next = await planModeLoop(task, backendUrl, ask);
1565
+ if (next === "BUILD") {
1566
+ currentMode = "BUILD";
1567
+ continue;
1568
+ }
1569
+ if (next === "switch") {
1570
+ currentMode = await promptModeSwitch(currentMode, ask);
1571
+ continue;
1572
+ }
1573
+ return;
1574
+ }
1575
+ if (currentMode === "BUILD") {
1576
+ const next = await buildModeLoop(task, backendUrl, ask);
1577
+ if (next === "switch") {
1578
+ currentMode = await promptModeSwitch(currentMode, ask);
1579
+ continue;
1580
+ }
1581
+ return;
1582
+ }
1583
+ if (currentMode === "BRAINSTORM") {
1584
+ const next = await brainstormModeLoop(task, backendUrl, ask);
1585
+ if (next === "switch") {
1586
+ currentMode = await promptModeSwitch(currentMode, ask);
1587
+ continue;
1588
+ }
1437
1589
  return;
1438
- } catch {
1439
1590
  }
1440
- await new Promise((r) => setTimeout(r, 3e3));
1441
1591
  }
1442
- throw new Error(
1443
- `Docker daemon did not become ready within ${Math.round(timeoutMs / 1e3)}s.
1444
-
1445
- Please ensure Docker is running, then re-run:
1446
- buildwithnexus init`
1592
+ }
1593
+ function printAppHeader() {
1594
+ console.log(chalk3.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
1595
+ console.log(
1596
+ chalk3.cyan("\u2551") + chalk3.bold.white(" Nexus - Autonomous Agent Orchestration ") + chalk3.cyan("\u2551")
1597
+ );
1598
+ const versionLine = ` v${appVersion}`;
1599
+ console.log(
1600
+ chalk3.cyan("\u2551") + chalk3.dim(versionLine.padEnd(60)) + chalk3.cyan("\u2551")
1447
1601
  );
1602
+ console.log(chalk3.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
1603
+ console.log("");
1448
1604
  }
1449
- async function imageExistsLocally(image, tag) {
1450
- const ref = `${image}:${tag}`;
1451
- try {
1452
- await execa("docker", ["image", "inspect", ref]);
1453
- return true;
1454
- } catch {
1455
- return false;
1456
- }
1605
+ async function promptModeSwitch(current, ask) {
1606
+ const others = ["PLAN", "BUILD", "BRAINSTORM"].filter((m) => m !== current);
1607
+ console.log("");
1608
+ console.log(
1609
+ chalk3.gray("Switch to: ") + others.map((m, i) => chalk3.bold(`[${i + 1}] ${m}`)).join(chalk3.gray(" ")) + chalk3.gray(" [Enter to stay in ") + chalk3.bold(current) + chalk3.gray("]")
1610
+ );
1611
+ const answer = await ask(chalk3.gray("> "));
1612
+ const n = parseInt(answer.trim(), 10);
1613
+ if (n === 1) return others[0];
1614
+ if (n === 2) return others[1];
1615
+ return current;
1457
1616
  }
1458
- async function pullImage(image, tag) {
1459
- const ref = `${image}:${tag}`;
1460
- log.step(`Pulling image ${ref}...`);
1617
+ async function planModeLoop(task, backendUrl, ask) {
1618
+ console.log(chalk3.bold("Task:"), chalk3.white(task));
1619
+ console.log("");
1620
+ console.log(chalk3.yellow("\u23F3 Fetching plan from backend..."));
1621
+ let steps = [];
1622
+ const keys = loadApiKeys();
1461
1623
  try {
1462
- await execa("docker", ["pull", ref], { stdio: "inherit" });
1463
- log.success(`Image ${ref} pulled`);
1624
+ const response = await fetch(`${backendUrl}/api/run`, {
1625
+ method: "POST",
1626
+ headers: { "Content-Type": "application/json" },
1627
+ body: JSON.stringify({ task, agent_role: "engineer", agent_goal: "", api_key: keys.anthropic || "", openai_api_key: keys.openai || "", google_api_key: keys.google || "" }),
1628
+ signal: AbortSignal.timeout(12e4)
1629
+ });
1630
+ if (!response.ok) {
1631
+ console.error(chalk3.red("Backend error \u2014 cannot fetch plan."));
1632
+ return "cancel";
1633
+ }
1634
+ const { run_id } = await response.json();
1635
+ tui.displayConnected(run_id);
1636
+ const streamResponse = await fetch(`${backendUrl}/api/stream/${run_id}`, { signal: AbortSignal.timeout(12e4) });
1637
+ const reader = streamResponse.body?.getReader();
1638
+ if (!reader) throw new Error("No response body");
1639
+ let planReceived = false;
1640
+ for await (const parsed of parseSSEStream2(reader)) {
1641
+ if (parsed.type === "plan") {
1642
+ steps = parsed.data["steps"] || [];
1643
+ planReceived = true;
1644
+ reader.cancel();
1645
+ break;
1646
+ } else if (parsed.type === "error") {
1647
+ const errorMsg = parsed.data["error"] || parsed.data["content"] || "Unknown error";
1648
+ tui.displayError(errorMsg);
1649
+ reader.cancel();
1650
+ return "cancel";
1651
+ }
1652
+ }
1653
+ if (!planReceived || steps.length === 0) {
1654
+ console.log(chalk3.yellow("No plan received from backend."));
1655
+ steps = ["(no steps returned \u2014 execute anyway?)"];
1656
+ }
1464
1657
  } catch (err) {
1465
- log.error(`Failed to pull image ${ref}`);
1466
- throw err;
1658
+ const msg = err instanceof Error ? err.message : String(err);
1659
+ console.error(chalk3.red("Error: " + msg));
1660
+ return "cancel";
1661
+ }
1662
+ displayPlanSteps(steps);
1663
+ while (true) {
1664
+ console.log(chalk3.gray("Options: ") + chalk3.bold("[Y]") + chalk3.gray(" Execute ") + chalk3.bold("[e]") + chalk3.gray(" Edit step ") + chalk3.bold("[s]") + chalk3.gray(" Switch mode ") + chalk3.bold("[Esc/n]") + chalk3.gray(" Cancel"));
1665
+ const answer = (await ask(tui.displayPermissionPrompt("Execute this plan?"))).trim().toLowerCase();
1666
+ if (answer === "" || answer === "y") {
1667
+ return "BUILD";
1668
+ }
1669
+ if (answer === "n" || answer === "\x1B") {
1670
+ console.log(chalk3.yellow("\nExecution cancelled.\n"));
1671
+ return "cancel";
1672
+ }
1673
+ if (answer === "e" || answer === "edit") {
1674
+ steps = await editPlanSteps(steps, ask);
1675
+ displayPlanSteps(steps);
1676
+ continue;
1677
+ }
1678
+ if (answer === "s" || answer === "switch") {
1679
+ return "switch";
1680
+ }
1467
1681
  }
1468
1682
  }
1469
- async function startNexus(keys, config) {
1470
- log.step("Starting NEXUS container...");
1683
+ function displayPlanSteps(steps) {
1684
+ console.log("");
1685
+ console.log(chalk3.bold.cyan("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
1686
+ console.log(chalk3.bold.cyan("\u2502") + chalk3.bold.white(" \u{1F4CB} Execution Plan ") + chalk3.bold.cyan("\u2502"));
1687
+ console.log(chalk3.bold.cyan("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
1688
+ steps.forEach((step, i) => {
1689
+ const label = ` Step ${i + 1}: `;
1690
+ const maxContentWidth = 57 - label.length;
1691
+ const truncated = step.length > maxContentWidth ? step.substring(0, maxContentWidth - 3) + "..." : step;
1692
+ const line = label + truncated;
1693
+ const padded = line.padEnd(57);
1694
+ console.log(chalk3.bold.cyan("\u2502") + chalk3.white(padded) + chalk3.bold.cyan("\u2502"));
1695
+ });
1696
+ console.log(chalk3.bold.cyan("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
1697
+ console.log("");
1698
+ }
1699
+ async function editPlanSteps(steps, ask) {
1700
+ console.log(chalk3.gray("Enter step number to edit, or press Enter to finish editing:"));
1701
+ const numStr = await ask(chalk3.bold("Step #: "));
1702
+ const n = parseInt(numStr.trim(), 10);
1703
+ if (!isNaN(n) && n >= 1 && n <= steps.length) {
1704
+ console.log(chalk3.gray(`Current: ${steps[n - 1]}`));
1705
+ const updated = await ask(chalk3.bold("New text: "));
1706
+ if (updated.trim()) steps[n - 1] = updated.trim();
1707
+ }
1708
+ return steps;
1709
+ }
1710
+ async function buildModeLoop(task, backendUrl, ask) {
1711
+ console.log(chalk3.bold("Task:"), chalk3.white(task));
1712
+ tui.displayConnecting();
1713
+ const keys = loadApiKeys();
1471
1714
  try {
1472
- await execa("docker", [
1473
- "run",
1474
- "-d",
1475
- "--name",
1476
- CONTAINER_NAME,
1477
- "-e",
1478
- `ANTHROPIC_API_KEY=${keys.anthropic}`,
1479
- "-e",
1480
- `OPENAI_API_KEY=${keys.openai}`,
1481
- "-p",
1482
- `${config.port}:${config.port}`,
1483
- "buildwithnexus/nexus:latest"
1484
- ]);
1485
- log.success(`NEXUS container started on port ${config.port}`);
1715
+ const response = await fetch(`${backendUrl}/api/run`, {
1716
+ method: "POST",
1717
+ headers: { "Content-Type": "application/json" },
1718
+ body: JSON.stringify({ task, agent_role: "engineer", agent_goal: "", api_key: keys.anthropic || "", openai_api_key: keys.openai || "", google_api_key: keys.google || "" }),
1719
+ signal: AbortSignal.timeout(12e4)
1720
+ });
1721
+ if (!response.ok) {
1722
+ console.error(chalk3.red("Backend error"));
1723
+ return "done";
1724
+ }
1725
+ const { run_id } = await response.json();
1726
+ tui.displayConnected(run_id);
1727
+ console.log(chalk3.bold.green("\u2699\uFE0F Executing..."));
1728
+ tui.displayStreamStart();
1729
+ const streamResponse = await fetch(`${backendUrl}/api/stream/${run_id}`, { signal: AbortSignal.timeout(12e4) });
1730
+ const reader = streamResponse.body?.getReader();
1731
+ if (!reader) throw new Error("No response body");
1732
+ for await (const parsed of parseSSEStream2(reader)) {
1733
+ const type = parsed.type;
1734
+ if (type === "execution_complete") {
1735
+ const summary = parsed.data["summary"] || "";
1736
+ const count = parsed.data["todos_completed"] || 0;
1737
+ tui.displayResults(summary, count);
1738
+ tui.displayComplete(tui.getElapsedTime());
1739
+ break;
1740
+ } else if (type === "done") {
1741
+ tui.displayEvent(type, { content: "Task completed successfully" });
1742
+ tui.displayComplete(tui.getElapsedTime());
1743
+ break;
1744
+ } else if (type === "error") {
1745
+ const errorMsg = parsed.data["error"] || parsed.data["content"] || "Unknown error";
1746
+ tui.displayError(errorMsg);
1747
+ break;
1748
+ } else if (type !== "plan") {
1749
+ tui.displayEvent(type, parsed.data);
1750
+ }
1751
+ }
1486
1752
  } catch (err) {
1487
- log.error("Failed to start NEXUS container");
1488
- throw err;
1753
+ const msg = err instanceof Error ? err.message : String(err);
1754
+ console.error(chalk3.red("Error: " + msg));
1755
+ }
1756
+ console.log("");
1757
+ console.log(
1758
+ chalk3.gray("Options: ") + chalk3.bold("[Enter]") + chalk3.gray(" Done ") + chalk3.bold("[s]") + chalk3.gray(" Switch mode")
1759
+ );
1760
+ const answer = (await ask(chalk3.bold("> "))).trim().toLowerCase();
1761
+ if (answer === "s" || answer === "switch") return "switch";
1762
+ return "done";
1763
+ }
1764
+ async function brainstormModeLoop(task, backendUrl, ask) {
1765
+ console.log(chalk3.bold("Starting topic:"), chalk3.white(task));
1766
+ console.log(chalk3.gray('Ask follow-up questions. Type "done" to exit, "switch" to change mode.\n'));
1767
+ let currentQuestion = task;
1768
+ while (true) {
1769
+ console.log(chalk3.bold.blue("\u{1F4A1} Thinking..."));
1770
+ try {
1771
+ const keys = loadApiKeys();
1772
+ const response = await fetch(`${backendUrl}/api/run`, {
1773
+ method: "POST",
1774
+ headers: { "Content-Type": "application/json" },
1775
+ body: JSON.stringify({
1776
+ task: currentQuestion,
1777
+ agent_role: "brainstorm",
1778
+ agent_goal: "Generate ideas, considerations, and suggestions. Be concise and helpful.",
1779
+ api_key: keys.anthropic || "",
1780
+ openai_api_key: keys.openai || "",
1781
+ google_api_key: keys.google || ""
1782
+ }),
1783
+ signal: AbortSignal.timeout(12e4)
1784
+ });
1785
+ if (response.ok) {
1786
+ const { run_id } = await response.json();
1787
+ const streamResponse = await fetch(`${backendUrl}/api/stream/${run_id}`, { signal: AbortSignal.timeout(12e4) });
1788
+ const reader = streamResponse.body?.getReader();
1789
+ if (reader) {
1790
+ let responseText = "";
1791
+ for await (const parsed of parseSSEStream2(reader)) {
1792
+ const type = parsed.type;
1793
+ const data = parsed.data;
1794
+ if (type === "done" || type === "execution_complete") {
1795
+ const summary = data["summary"] || "";
1796
+ if (summary) responseText = summary;
1797
+ break;
1798
+ } else if (type === "error") {
1799
+ const errorMsg = data["error"] || data["content"] || "Unknown error";
1800
+ responseText += errorMsg + "\n";
1801
+ break;
1802
+ } else if (type === "thought" || type === "observation") {
1803
+ const content = data["content"] || "";
1804
+ if (content) responseText += content + "\n";
1805
+ } else if (type === "agent_response" || type === "agent_result") {
1806
+ const content = data["content"] || data["result"] || "";
1807
+ if (content) responseText += content + "\n";
1808
+ } else if (type === "action") {
1809
+ const content = data["content"] || "";
1810
+ if (content) responseText += content + "\n";
1811
+ } else if (type === "agent_working") {
1812
+ } else if (type !== "plan") {
1813
+ const content = data["content"] || data["response"] || "";
1814
+ if (content) responseText += content + "\n";
1815
+ }
1816
+ }
1817
+ if (responseText.trim()) {
1818
+ tui.displayBrainstormResponse(responseText.trim());
1819
+ } else {
1820
+ console.log(chalk3.gray("(No response received from agent)"));
1821
+ }
1822
+ }
1823
+ } else {
1824
+ console.log(chalk3.red("Could not reach backend for brainstorm response."));
1825
+ }
1826
+ } catch (err) {
1827
+ const msg = err instanceof Error ? err.message : String(err);
1828
+ console.error(chalk3.red("Error: " + msg));
1829
+ }
1830
+ const followUp = await ask(chalk3.bold.blue("\u{1F4AC} You: "));
1831
+ const lower = followUp.trim().toLowerCase();
1832
+ if (lower === "done" || lower === "exit") return "done";
1833
+ if (lower === "switch") return "switch";
1834
+ if (!followUp.trim()) continue;
1835
+ currentQuestion = followUp.trim();
1489
1836
  }
1490
1837
  }
1491
- async function stopNexus() {
1492
- log.step("Stopping NEXUS container...");
1493
- try {
1494
- await execa("docker", ["rm", "-f", CONTAINER_NAME]);
1495
- log.success("NEXUS container stopped and removed");
1496
- } catch (err) {
1497
- log.error("Failed to stop NEXUS container");
1498
- throw err;
1499
- }
1838
+
1839
+ // src/commands/install.ts
1840
+ import { Command } from "commander";
1841
+ import { execa as execa2 } from "execa";
1842
+
1843
+ // src/ui/spinner.ts
1844
+ import ora from "ora";
1845
+ import chalk4 from "chalk";
1846
+ function createSpinner(text) {
1847
+ return ora({ text, color: "cyan", spinner: "dots" });
1500
1848
  }
1501
- async function dockerExec(command) {
1502
- try {
1503
- const { stdout, stderr } = await execa("docker", ["exec", CONTAINER_NAME, "sh", "-c", command]);
1504
- return { stdout, stderr, code: 0 };
1505
- } catch (err) {
1506
- const e = err;
1507
- return { stdout: e.stdout ?? "", stderr: e.stderr ?? "", code: e.exitCode ?? 1 };
1508
- }
1849
+ function succeed(spinner, text) {
1850
+ spinner.succeed(chalk4.green(text));
1509
1851
  }
1510
- async function isNexusRunning() {
1511
- try {
1512
- const { stdout } = await execa("docker", [
1513
- "ps",
1514
- "--filter",
1515
- `name=^/${CONTAINER_NAME}$`,
1516
- "--format",
1517
- "{{.Names}}"
1518
- ]);
1519
- return stdout.trim() === CONTAINER_NAME;
1520
- } catch {
1521
- return false;
1522
- }
1852
+ function fail(spinner, text) {
1853
+ spinner.fail(chalk4.red(text));
1523
1854
  }
1524
1855
 
1525
1856
  // src/commands/install.ts
1857
+ init_logger();
1858
+ init_platform();
1859
+ init_docker();
1526
1860
  var installCommand = new Command("install").description("Install Docker and configure system prerequisites").action(async () => {
1527
1861
  const spinner = createSpinner("");
1528
1862
  log.step("Checking Docker installation...");
@@ -1568,152 +1902,12 @@ var installCommand = new Command("install").description("Install Docker and conf
1568
1902
  init_banner();
1569
1903
  import { Command as Command2 } from "commander";
1570
1904
  import chalk7 from "chalk";
1905
+ init_logger();
1571
1906
 
1572
1907
  // src/ui/prompts.ts
1908
+ init_dlp();
1573
1909
  import { confirm, password } from "@inquirer/prompts";
1574
1910
  import chalk6 from "chalk";
1575
-
1576
- // src/core/dlp.ts
1577
- import crypto from "crypto";
1578
- import fs2 from "fs";
1579
- import path4 from "path";
1580
- var NEXUS_HOME = path4.join(process.env.HOME || "~", ".buildwithnexus");
1581
- var SECRET_PATTERNS = [
1582
- /sk-ant-api03-[A-Za-z0-9_-]{20,}/g,
1583
- // Anthropic API key
1584
- /sk-[A-Za-z0-9]{20,}/g,
1585
- // OpenAI API key
1586
- /AIza[A-Za-z0-9_-]{35}/g
1587
- // Google AI API key
1588
- ];
1589
- var FORBIDDEN_KEY_CHARS = /[\n\r\t'"\\`${}();&|<>!#%^]/;
1590
- var KEY_VALIDATORS = {
1591
- ANTHROPIC_API_KEY: /^sk-ant-[A-Za-z0-9_-]{20,}$/,
1592
- OPENAI_API_KEY: /^sk-[A-Za-z0-9_-]{20,}$/,
1593
- GOOGLE_API_KEY: /^AIza[A-Za-z0-9_-]{35,}$/,
1594
- NEXUS_MASTER_SECRET: /^[A-Za-z0-9_-]{20,64}$/
1595
- };
1596
- function shellEscape(value) {
1597
- if (value.includes("\0")) {
1598
- throw new DlpViolation("Null byte in shell argument");
1599
- }
1600
- return "'" + value.replace(/'/g, "'\\''") + "'";
1601
- }
1602
- function shellCommand(strings, ...values) {
1603
- let result = strings[0];
1604
- for (let i = 0; i < values.length; i++) {
1605
- result += shellEscape(values[i]) + strings[i + 1];
1606
- }
1607
- return result;
1608
- }
1609
- function redact(text) {
1610
- let result = text;
1611
- for (const pattern of SECRET_PATTERNS) {
1612
- pattern.lastIndex = 0;
1613
- result = result.replace(pattern, "[REDACTED]");
1614
- }
1615
- return result;
1616
- }
1617
- function redactError(err) {
1618
- if (err instanceof Error) {
1619
- const safe = new Error(redact(err.message));
1620
- safe.name = err.name;
1621
- if (err.stack) safe.stack = redact(err.stack);
1622
- return safe;
1623
- }
1624
- return new Error(redact(String(err)));
1625
- }
1626
- var DlpViolation = class extends Error {
1627
- constructor(message) {
1628
- super(message);
1629
- this.name = "DlpViolation";
1630
- }
1631
- };
1632
- function validateKeyValue(keyName, value) {
1633
- if (FORBIDDEN_KEY_CHARS.test(value)) {
1634
- throw new DlpViolation(
1635
- `${keyName} contains characters that are not permitted in API keys`
1636
- );
1637
- }
1638
- if (value.length < 10 || value.length > 256) {
1639
- throw new DlpViolation(
1640
- `${keyName} length out of expected range (10-256 characters)`
1641
- );
1642
- }
1643
- const validator = KEY_VALIDATORS[keyName];
1644
- if (validator && !validator.test(value)) {
1645
- throw new DlpViolation(
1646
- `${keyName} does not match the expected format for this key type`
1647
- );
1648
- }
1649
- }
1650
- function validateAllKeys(keys) {
1651
- const violations = [];
1652
- for (const [name, value] of Object.entries(keys)) {
1653
- if (!value) continue;
1654
- try {
1655
- validateKeyValue(name, value);
1656
- } catch (err) {
1657
- if (err instanceof DlpViolation) violations.push(err.message);
1658
- }
1659
- }
1660
- return violations;
1661
- }
1662
- var HMAC_PATH = path4.join(NEXUS_HOME, ".keys.hmac");
1663
- function computeFileHmac(filePath, secret) {
1664
- const content = fs2.readFileSync(filePath);
1665
- return crypto.createHmac("sha256", secret).update(content).digest("hex");
1666
- }
1667
- function sealKeysFile(keysPath, masterSecret) {
1668
- const hmac = computeFileHmac(keysPath, masterSecret);
1669
- fs2.writeFileSync(HMAC_PATH, hmac, { mode: 384 });
1670
- }
1671
- function verifyKeysFile(keysPath, masterSecret) {
1672
- const keysExist = fs2.existsSync(keysPath);
1673
- const hmacExist = fs2.existsSync(HMAC_PATH);
1674
- if (!keysExist && !hmacExist) return true;
1675
- if (keysExist && !hmacExist) return false;
1676
- try {
1677
- const stored = fs2.readFileSync(HMAC_PATH, "utf-8").trim();
1678
- const computed = computeFileHmac(keysPath, masterSecret);
1679
- return crypto.timingSafeEqual(
1680
- Buffer.from(stored, "hex"),
1681
- Buffer.from(computed, "hex")
1682
- );
1683
- } catch {
1684
- return false;
1685
- }
1686
- }
1687
- var AUDIT_PATH = path4.join(NEXUS_HOME, "audit.log");
1688
- var MAX_AUDIT_SIZE = 10 * 1024 * 1024;
1689
- function audit(event, detail = "") {
1690
- try {
1691
- const dir = path4.dirname(AUDIT_PATH);
1692
- if (!fs2.existsSync(dir)) return;
1693
- if (fs2.existsSync(AUDIT_PATH)) {
1694
- const stat = fs2.statSync(AUDIT_PATH);
1695
- if (stat.size > MAX_AUDIT_SIZE) {
1696
- const rotated = AUDIT_PATH + ".1";
1697
- if (fs2.existsSync(rotated)) fs2.unlinkSync(rotated);
1698
- fs2.renameSync(AUDIT_PATH, rotated);
1699
- try {
1700
- fs2.chmodSync(rotated, 384);
1701
- } catch {
1702
- }
1703
- }
1704
- }
1705
- const line = `${(/* @__PURE__ */ new Date()).toISOString()} | ${event} | ${redact(detail)}
1706
- `;
1707
- fs2.appendFileSync(AUDIT_PATH, line, { mode: 384 });
1708
- try {
1709
- fs2.chmodSync(AUDIT_PATH, 384);
1710
- } catch {
1711
- }
1712
- } catch {
1713
- }
1714
- }
1715
-
1716
- // src/ui/prompts.ts
1717
1911
  async function promptInitConfig() {
1718
1912
  console.log(chalk6.bold("\n API Keys\n"));
1719
1913
  const anthropicKey = await password({
@@ -1768,66 +1962,14 @@ async function promptInitConfig() {
1768
1962
  };
1769
1963
  }
1770
1964
 
1771
- // src/core/secrets.ts
1772
- import fs3 from "fs";
1773
- import path5 from "path";
1774
- import crypto2 from "crypto";
1775
- var NEXUS_HOME2 = process.env.NEXUS_HOME || path5.join(process.env.HOME || "~", ".buildwithnexus");
1776
- var CONFIG_PATH = process.env.NEXUS_CONFIG_PATH || path5.join(NEXUS_HOME2, "config.json");
1777
- var KEYS_PATH = process.env.NEXUS_KEYS_PATH || path5.join(NEXUS_HOME2, ".env.keys");
1778
- function ensureHome() {
1779
- fs3.mkdirSync(NEXUS_HOME2, { recursive: true, mode: 448 });
1780
- fs3.mkdirSync(path5.join(NEXUS_HOME2, "vm", "images"), { recursive: true, mode: 448 });
1781
- fs3.mkdirSync(path5.join(NEXUS_HOME2, "vm", "configs"), { recursive: true, mode: 448 });
1782
- fs3.mkdirSync(path5.join(NEXUS_HOME2, "vm", "logs"), { recursive: true, mode: 448 });
1783
- fs3.mkdirSync(path5.join(NEXUS_HOME2, "ssh"), { recursive: true, mode: 448 });
1784
- }
1785
- function generateMasterSecret() {
1786
- return crypto2.randomBytes(32).toString("base64url");
1787
- }
1788
- function saveConfig(config) {
1789
- const { masterSecret: _secret, ...safeConfig } = config;
1790
- fs3.writeFileSync(CONFIG_PATH, JSON.stringify(safeConfig, null, 2), { mode: 384 });
1791
- }
1792
- function loadConfig() {
1793
- if (!fs3.existsSync(CONFIG_PATH)) return null;
1794
- return JSON.parse(fs3.readFileSync(CONFIG_PATH, "utf-8"));
1795
- }
1796
- function saveKeys(keys) {
1797
- const violations = validateAllKeys(keys);
1798
- if (violations.length > 0) {
1799
- throw new DlpViolation(`Key validation failed: ${violations.join("; ")}`);
1800
- }
1801
- const lines = Object.entries(keys).filter(([, v]) => v).map(([k, v]) => `${k}=${v}`);
1802
- fs3.writeFileSync(KEYS_PATH, lines.join("\n") + "\n", { mode: 384 });
1803
- sealKeysFile(KEYS_PATH, keys.NEXUS_MASTER_SECRET);
1804
- audit("keys_saved", `${Object.keys(keys).filter((k) => keys[k]).length} keys saved`);
1805
- }
1806
- function loadKeys() {
1807
- if (!fs3.existsSync(KEYS_PATH)) return null;
1808
- const content = fs3.readFileSync(KEYS_PATH, "utf-8");
1809
- const keys = {};
1810
- for (const line of content.split("\n")) {
1811
- const eq = line.indexOf("=");
1812
- if (eq > 0) keys[line.slice(0, eq)] = line.slice(eq + 1);
1813
- }
1814
- const result = keys;
1815
- if (result.NEXUS_MASTER_SECRET && !verifyKeysFile(KEYS_PATH, result.NEXUS_MASTER_SECRET)) {
1816
- audit("keys_tampered", "HMAC mismatch on .env.keys");
1817
- throw new DlpViolation(
1818
- ".env.keys has been modified outside of buildwithnexus. Run 'buildwithnexus keys set' to re-enter your keys, or 'buildwithnexus destroy' to start fresh."
1819
- );
1820
- }
1821
- audit("keys_loaded", `${Object.keys(keys).length} keys loaded`);
1822
- return result;
1823
- }
1824
- function maskKey(key) {
1825
- if (key.length <= 8) return "***";
1826
- const reveal = Math.min(4, Math.floor(key.length * 0.1));
1827
- return key.slice(0, reveal) + "..." + key.slice(-reveal);
1828
- }
1965
+ // src/commands/init.ts
1966
+ init_platform();
1967
+ init_secrets();
1968
+ init_docker();
1829
1969
 
1830
1970
  // src/core/tunnel.ts
1971
+ init_docker();
1972
+ init_dlp();
1831
1973
  var CLOUDFLARED_VERSION = "2024.12.2";
1832
1974
  var CLOUDFLARED_SHA256 = {
1833
1975
  amd64: "40ec9a0f5b58e3b04183aaf01c4ddd4dbc6af39b0f06be4b7ce8b1011d0a07ab",
@@ -1871,6 +2013,7 @@ async function startTunnel() {
1871
2013
  }
1872
2014
 
1873
2015
  // src/commands/init.ts
2016
+ init_dlp();
1874
2017
  async function withSpinner(spinner, label, fn) {
1875
2018
  spinner.text = label;
1876
2019
  spinner.start();
@@ -1903,7 +2046,6 @@ var phases = [
1903
2046
  {
1904
2047
  name: "Configuration",
1905
2048
  run: async (ctx) => {
1906
- showBanner();
1907
2049
  const platform = detectPlatform();
1908
2050
  log.detail("Platform", `${platform.os} ${platform.arch}`);
1909
2051
  const userConfig = await promptInitConfig();
@@ -2086,8 +2228,12 @@ var initCommand = new Command2("init").description("Scaffold and launch a new NE
2086
2228
 
2087
2229
  // src/commands/start.ts
2088
2230
  import { Command as Command3 } from "commander";
2231
+ init_logger();
2232
+ init_secrets();
2233
+ init_docker();
2089
2234
 
2090
2235
  // src/core/health.ts
2236
+ init_docker();
2091
2237
  async function waitForServer(timeoutMs = 9e5) {
2092
2238
  const start = Date.now();
2093
2239
  let lastLog = 0;
@@ -2135,8 +2281,13 @@ var startCommand = new Command3("start").description("Start the NEXUS runtime").
2135
2281
  succeed(spinner, "Image ready");
2136
2282
  spinner = createSpinner("Starting NEXUS container...");
2137
2283
  spinner.start();
2284
+ const keys = loadKeys();
2285
+ if (!keys) {
2286
+ fail(spinner, "No API keys found. Run: buildwithnexus init");
2287
+ process.exit(1);
2288
+ }
2138
2289
  await startNexus(
2139
- { anthropic: config.anthropicKey, openai: config.openaiKey },
2290
+ { anthropic: keys.ANTHROPIC_API_KEY, openai: keys.OPENAI_API_KEY || "" },
2140
2291
  { port: config.httpPort }
2141
2292
  );
2142
2293
  succeed(spinner, "Container started");
@@ -2163,6 +2314,7 @@ var startCommand = new Command3("start").description("Start the NEXUS runtime").
2163
2314
 
2164
2315
  // src/commands/stop.ts
2165
2316
  import { Command as Command4 } from "commander";
2317
+ init_logger();
2166
2318
  import { execFile } from "child_process";
2167
2319
  import { promisify } from "util";
2168
2320
  var execFileAsync = promisify(execFile);
@@ -2220,6 +2372,9 @@ var stopCommand = new Command4("stop").description("Gracefully shut down the NEX
2220
2372
  });
2221
2373
 
2222
2374
  // src/commands/status.ts
2375
+ init_logger();
2376
+ init_secrets();
2377
+ init_docker();
2223
2378
  import { Command as Command5 } from "commander";
2224
2379
  import chalk8 from "chalk";
2225
2380
  async function checkHttpHealth(port) {
@@ -2294,10 +2449,14 @@ var statusCommand = new Command5("status").description("Check NEXUS runtime heal
2294
2449
  });
2295
2450
 
2296
2451
  // src/commands/doctor.ts
2452
+ init_logger();
2453
+ init_platform();
2454
+ init_docker();
2455
+ init_secrets();
2297
2456
  import { Command as Command6 } from "commander";
2298
2457
  import chalk9 from "chalk";
2299
- import fs4 from "fs";
2300
- import path6 from "path";
2458
+ import fs3 from "fs";
2459
+ import path5 from "path";
2301
2460
  var doctorCommand = new Command6("doctor").description("Diagnose NEXUS runtime environment").action(async () => {
2302
2461
  const platform = detectPlatform();
2303
2462
  const check = (ok) => ok ? chalk9.green("\u2713") : chalk9.red("\u2717");
@@ -2313,7 +2472,7 @@ var doctorCommand = new Command6("doctor").description("Diagnose NEXUS runtime e
2313
2472
  } else {
2314
2473
  console.log(` ${check(false)} Docker not installed`);
2315
2474
  }
2316
- const keyExists = fs4.existsSync(path6.join(NEXUS_HOME2, "ssh", "id_nexus_vm"));
2475
+ const keyExists = fs3.existsSync(path5.join(NEXUS_HOME2, "ssh", "id_nexus_vm"));
2317
2476
  console.log(` ${check(keyExists)} SSH key`);
2318
2477
  const config = loadConfig();
2319
2478
  console.log(` ${check(!!config)} Configuration`);
@@ -2345,6 +2504,7 @@ var doctorCommand = new Command6("doctor").description("Diagnose NEXUS runtime e
2345
2504
  });
2346
2505
 
2347
2506
  // src/commands/logs.ts
2507
+ init_logger();
2348
2508
  import { Command as Command7 } from "commander";
2349
2509
  import { execa as execa3 } from "execa";
2350
2510
  var logsCommand = new Command7("logs").description("View NEXUS server logs").action(async () => {
@@ -2378,16 +2538,19 @@ var logsCommand = new Command7("logs").description("View NEXUS server logs").act
2378
2538
 
2379
2539
  // src/commands/update.ts
2380
2540
  import { Command as Command8 } from "commander";
2381
- import path7 from "path";
2382
- import fs5 from "fs";
2383
- import { fileURLToPath as fileURLToPath3 } from "url";
2541
+ import path6 from "path";
2542
+ import fs4 from "fs";
2543
+ import { fileURLToPath as fileURLToPath2 } from "url";
2544
+ init_logger();
2545
+ init_secrets();
2546
+ init_docker();
2384
2547
  import { execa as execa4 } from "execa";
2385
2548
  function getReleaseTarball() {
2386
- const dir = path7.dirname(fileURLToPath3(import.meta.url));
2387
- const tarballPath = path7.join(dir, "nexus-release.tar.gz");
2388
- if (fs5.existsSync(tarballPath)) return tarballPath;
2389
- const rootPath = path7.resolve(dir, "..", "dist", "nexus-release.tar.gz");
2390
- if (fs5.existsSync(rootPath)) return rootPath;
2549
+ const dir = path6.dirname(fileURLToPath2(import.meta.url));
2550
+ const tarballPath = path6.join(dir, "nexus-release.tar.gz");
2551
+ if (fs4.existsSync(tarballPath)) return tarballPath;
2552
+ const rootPath = path6.resolve(dir, "..", "dist", "nexus-release.tar.gz");
2553
+ if (fs4.existsSync(rootPath)) return rootPath;
2391
2554
  throw new Error("nexus-release.tar.gz not found. Reinstall buildwithnexus to get the latest release.");
2392
2555
  }
2393
2556
  var updateCommand = new Command8("update").description("Update NEXUS to the latest bundled release and restart").action(async () => {
@@ -2438,9 +2601,12 @@ var updateCommand = new Command8("update").description("Update NEXUS to the late
2438
2601
  // src/commands/destroy.ts
2439
2602
  import { Command as Command9 } from "commander";
2440
2603
  import chalk10 from "chalk";
2441
- import fs6 from "fs";
2604
+ import fs5 from "fs";
2442
2605
  import { input } from "@inquirer/prompts";
2443
- import path8 from "path";
2606
+ init_logger();
2607
+ init_secrets();
2608
+ init_docker();
2609
+ import path7 from "path";
2444
2610
  var destroyCommand = new Command9("destroy").description("Remove NEXUS VM and all data").option("--force", "Skip confirmation").action(async (opts) => {
2445
2611
  const config = loadConfig();
2446
2612
  if (!opts.force) {
@@ -2467,9 +2633,9 @@ var destroyCommand = new Command9("destroy").description("Remove NEXUS VM and al
2467
2633
  }
2468
2634
  await stopNexus();
2469
2635
  }
2470
- const sshConfigPath = path8.join(process.env.HOME || "~", ".ssh", "config");
2471
- if (fs6.existsSync(sshConfigPath)) {
2472
- const content = fs6.readFileSync(sshConfigPath, "utf-8");
2636
+ const sshConfigPath = path7.join(process.env.HOME || "~", ".ssh", "config");
2637
+ if (fs5.existsSync(sshConfigPath)) {
2638
+ const content = fs5.readFileSync(sshConfigPath, "utf-8");
2473
2639
  const lines = content.split("\n");
2474
2640
  const filtered = [];
2475
2641
  let skip = false;
@@ -2482,14 +2648,17 @@ var destroyCommand = new Command9("destroy").description("Remove NEXUS VM and al
2482
2648
  skip = false;
2483
2649
  filtered.push(line);
2484
2650
  }
2485
- fs6.writeFileSync(sshConfigPath, filtered.join("\n"));
2651
+ fs5.writeFileSync(sshConfigPath, filtered.join("\n"));
2486
2652
  }
2487
- fs6.rmSync(NEXUS_HOME2, { recursive: true, force: true });
2653
+ fs5.rmSync(NEXUS_HOME2, { recursive: true, force: true });
2488
2654
  succeed(spinner, "NEXUS runtime destroyed");
2489
2655
  log.dim("Run 'buildwithnexus init' to set up again");
2490
2656
  });
2491
2657
 
2492
2658
  // src/commands/keys.ts
2659
+ init_logger();
2660
+ init_secrets();
2661
+ init_dlp();
2493
2662
  import { Command as Command10 } from "commander";
2494
2663
  import { password as password2 } from "@inquirer/prompts";
2495
2664
  import chalk11 from "chalk";
@@ -2548,6 +2717,8 @@ keysCommand.command("set <key>").description("Set or update an API key (e.g. ANT
2548
2717
  });
2549
2718
 
2550
2719
  // src/commands/ssh.ts
2720
+ init_logger();
2721
+ init_docker();
2551
2722
  import { Command as Command11 } from "commander";
2552
2723
  import { execa as execa5 } from "execa";
2553
2724
  var sshCommand = new Command11("ssh").description("Open an interactive shell inside the NEXUS container").action(async () => {
@@ -2561,9 +2732,13 @@ var sshCommand = new Command11("ssh").description("Open an interactive shell ins
2561
2732
  });
2562
2733
 
2563
2734
  // src/commands/brainstorm.ts
2735
+ init_logger();
2564
2736
  import { Command as Command12 } from "commander";
2565
2737
  import chalk12 from "chalk";
2566
2738
  import { input as input2 } from "@inquirer/prompts";
2739
+ init_secrets();
2740
+ init_docker();
2741
+ init_dlp();
2567
2742
  var COS_PREFIX = chalk12.bold.cyan(" Chief of Staff");
2568
2743
  var YOU_PREFIX = chalk12.bold.white(" You");
2569
2744
  var DIVIDER = chalk12.dim(" " + "\u2500".repeat(56));
@@ -2682,9 +2857,13 @@ var brainstormCommand = new Command12("brainstorm").description("Brainstorm an i
2682
2857
  });
2683
2858
 
2684
2859
  // src/commands/ninety-nine.ts
2860
+ init_logger();
2685
2861
  import { Command as Command13 } from "commander";
2686
2862
  import chalk13 from "chalk";
2687
2863
  import { input as input3 } from "@inquirer/prompts";
2864
+ init_secrets();
2865
+ init_docker();
2866
+ init_dlp();
2688
2867
  var AGENT_PREFIX = chalk13.bold.green(" 99 \u276F");
2689
2868
  var YOU_PREFIX2 = chalk13.bold.white(" You");
2690
2869
  var DIVIDER2 = chalk13.dim(" " + "\u2500".repeat(56));
@@ -2872,15 +3051,20 @@ var ninetyNineCommand = new Command13("99").description("AI pair-programming ses
2872
3051
  });
2873
3052
 
2874
3053
  // src/commands/shell.ts
3054
+ init_logger();
2875
3055
  import { Command as Command14 } from "commander";
2876
3056
  import chalk16 from "chalk";
3057
+ init_secrets();
3058
+ init_docker();
3059
+ init_dlp();
2877
3060
 
2878
3061
  // src/ui/repl.ts
3062
+ init_secrets();
2879
3063
  import readline3 from "readline";
2880
- import fs7 from "fs";
2881
- import path9 from "path";
3064
+ import fs6 from "fs";
3065
+ import path8 from "path";
2882
3066
  import chalk14 from "chalk";
2883
- var HISTORY_FILE = path9.join(NEXUS_HOME2, "shell_history");
3067
+ var HISTORY_FILE = path8.join(NEXUS_HOME2, "shell_history");
2884
3068
  var MAX_HISTORY = 1e3;
2885
3069
  var Repl = class {
2886
3070
  rl = null;
@@ -2896,17 +3080,17 @@ var Repl = class {
2896
3080
  }
2897
3081
  loadHistory() {
2898
3082
  try {
2899
- if (fs7.existsSync(HISTORY_FILE)) {
2900
- this.history = fs7.readFileSync(HISTORY_FILE, "utf-8").split("\n").filter(Boolean).slice(-MAX_HISTORY);
3083
+ if (fs6.existsSync(HISTORY_FILE)) {
3084
+ this.history = fs6.readFileSync(HISTORY_FILE, "utf-8").split("\n").filter(Boolean).slice(-MAX_HISTORY);
2901
3085
  }
2902
3086
  } catch {
2903
3087
  }
2904
3088
  }
2905
3089
  saveHistory() {
2906
3090
  try {
2907
- const dir = path9.dirname(HISTORY_FILE);
2908
- fs7.mkdirSync(dir, { recursive: true });
2909
- fs7.writeFileSync(HISTORY_FILE, this.history.slice(-MAX_HISTORY).join("\n") + "\n", { mode: 384 });
3091
+ const dir = path8.dirname(HISTORY_FILE);
3092
+ fs6.mkdirSync(dir, { recursive: true });
3093
+ fs6.writeFileSync(HISTORY_FILE, this.history.slice(-MAX_HISTORY).join("\n") + "\n", { mode: 384 });
2910
3094
  } catch {
2911
3095
  }
2912
3096
  }
@@ -3002,6 +3186,9 @@ var Repl = class {
3002
3186
  };
3003
3187
 
3004
3188
  // src/core/event-stream.ts
3189
+ init_docker();
3190
+ init_secrets();
3191
+ init_dlp();
3005
3192
  import chalk15 from "chalk";
3006
3193
  var ROLE_COLORS = {
3007
3194
  "Chief of Staff": chalk15.bold.cyan,
@@ -3116,8 +3303,8 @@ var EventStream = class {
3116
3303
  };
3117
3304
 
3118
3305
  // src/commands/shell.ts
3119
- async function httpPost(httpPort, path12, body) {
3120
- const res = await fetch(`http://localhost:${httpPort}${path12}`, {
3306
+ async function httpPost(httpPort, path11, body) {
3307
+ const res = await fetch(`http://localhost:${httpPort}${path11}`, {
3121
3308
  method: "POST",
3122
3309
  headers: { "Content-Type": "application/json" },
3123
3310
  body: JSON.stringify(body),
@@ -3132,9 +3319,9 @@ async function httpPost(httpPort, path12, body) {
3132
3319
  return text;
3133
3320
  }
3134
3321
  }
3135
- async function httpGet(httpPort, path12) {
3322
+ async function httpGet(httpPort, path11) {
3136
3323
  try {
3137
- const res = await fetch(`http://localhost:${httpPort}${path12}`, {
3324
+ const res = await fetch(`http://localhost:${httpPort}${path11}`, {
3138
3325
  signal: AbortSignal.timeout(1e4)
3139
3326
  });
3140
3327
  const text = await res.text();
@@ -3551,49 +3738,19 @@ var shellCommand2 = new Command14("shell").description("Launch the interactive N
3551
3738
  }
3552
3739
  });
3553
3740
 
3554
- // src/cli.ts
3555
- function getVersionStatic() {
3556
- try {
3557
- const __dirname2 = dirname2(fileURLToPath4(import.meta.url));
3558
- const packagePath = join2(__dirname2, "..", "package.json");
3559
- const packageJson = JSON.parse(readFileSync2(packagePath, "utf-8"));
3560
- return packageJson.version;
3561
- } catch {
3562
- return true ? "0.7.1" : "0.0.0-unknown";
3563
- }
3564
- }
3565
- var cli = new Command15().name("buildwithnexus").description("Auto-scaffold and launch a fully autonomous NEXUS runtime").version(getVersionStatic());
3566
- cli.addCommand(installCommand);
3567
- cli.addCommand(initCommand);
3568
- cli.addCommand(startCommand);
3569
- cli.addCommand(stopCommand);
3570
- cli.addCommand(statusCommand);
3571
- cli.addCommand(doctorCommand);
3572
- cli.addCommand(logsCommand);
3573
- cli.addCommand(updateCommand);
3574
- cli.addCommand(destroyCommand);
3575
- cli.addCommand(keysCommand);
3576
- cli.addCommand(sshCommand);
3577
- cli.addCommand(brainstormCommand);
3578
- cli.addCommand(ninetyNineCommand);
3579
- cli.addCommand(shellCommand2);
3580
- cli.action(() => {
3581
- cli.help();
3582
- });
3583
-
3584
3741
  // src/core/update-notifier.ts
3585
- import fs8 from "fs";
3586
- import path10 from "path";
3587
- import os4 from "os";
3742
+ import fs7 from "fs";
3743
+ import path9 from "path";
3744
+ import os3 from "os";
3588
3745
  import https from "https";
3589
3746
  import chalk17 from "chalk";
3590
3747
  var PACKAGE_NAME = "buildwithnexus";
3591
3748
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
3592
- var STATE_DIR = path10.join(os4.homedir(), ".buildwithnexus");
3593
- var STATE_FILE = path10.join(STATE_DIR, ".update-check.json");
3749
+ var STATE_DIR = path9.join(os3.homedir(), ".buildwithnexus");
3750
+ var STATE_FILE = path9.join(STATE_DIR, ".update-check.json");
3594
3751
  function readState() {
3595
3752
  try {
3596
- const raw = fs8.readFileSync(STATE_FILE, "utf-8");
3753
+ const raw = fs7.readFileSync(STATE_FILE, "utf-8");
3597
3754
  return JSON.parse(raw);
3598
3755
  } catch {
3599
3756
  return { lastCheck: 0, latestVersion: null };
@@ -3601,8 +3758,8 @@ function readState() {
3601
3758
  }
3602
3759
  function writeState(state) {
3603
3760
  try {
3604
- fs8.mkdirSync(STATE_DIR, { recursive: true, mode: 448 });
3605
- fs8.writeFileSync(STATE_FILE, JSON.stringify(state), { mode: 384 });
3761
+ fs7.mkdirSync(STATE_DIR, { recursive: true, mode: 448 });
3762
+ fs7.writeFileSync(STATE_FILE, JSON.stringify(state), { mode: 384 });
3606
3763
  } catch {
3607
3764
  }
3608
3765
  }
@@ -3674,18 +3831,34 @@ function printUpdateBanner(current, latest) {
3674
3831
  process.stderr.write(msg + "\n");
3675
3832
  }
3676
3833
 
3834
+ // src/core/models.ts
3835
+ var MODELS = {
3836
+ OPUS: "claude-opus-4-6",
3837
+ SONNET: "claude-sonnet-4-6-20250929",
3838
+ HAIKU: "claude-haiku-4-5-20251001",
3839
+ DEFAULT: "claude-sonnet-4-6-20250929"
3840
+ };
3841
+
3677
3842
  // src/bin.ts
3678
3843
  import dotenv2 from "dotenv";
3679
- import os5 from "os";
3680
- import path11 from "path";
3681
- var homeEnvPath = path11.join(os5.homedir(), ".env.local");
3844
+ import os4 from "os";
3845
+ import path10 from "path";
3846
+ var homeEnvPath = path10.join(os4.homedir(), ".env.local");
3682
3847
  dotenv2.config({ path: homeEnvPath });
3683
- var version = true ? "0.7.1" : "0.5.17";
3848
+ var version = true ? "0.7.2" : pkg.version;
3684
3849
  checkForUpdates(version);
3685
3850
  program.name("buildwithnexus").description("Nexus - AI-Powered Task Execution").version(version);
3686
3851
  program.command("da-init").description("Initialize Nexus (set up API keys and .env.local)").action(deepAgentsInitCommand);
3687
- program.command("run <task>").description("Run a task with Nexus").option("-a, --agent <name>", "Agent role (engineer, researcher, etc)", "engineer").option("-g, --goal <goal>", "Agent goal").option("-m, --model <model>", "LLM model", "claude-sonnet-4-20250514").action(runCommand);
3852
+ program.command("run <task>").description("Run a task with Nexus").option("-a, --agent <name>", "Agent role (engineer, researcher, etc)", "engineer").option("-g, --goal <goal>", "Agent goal").option("-m, --model <model>", "LLM model", MODELS.DEFAULT).action(runCommand);
3688
3853
  program.command("dashboard").description("Start the Nexus dashboard server").option("-p, --port <port>", "Dashboard port", "4201").action(dashboardCommand);
3854
+ program.command("server").description("Start the Nexus backend server").action(async () => {
3855
+ const { startBackend: startBackend2 } = await Promise.resolve().then(() => (init_docker(), docker_exports));
3856
+ await startBackend2();
3857
+ const chalk18 = (await import("chalk")).default;
3858
+ console.log(chalk18.green("Backend server started. Press Ctrl+C to stop."));
3859
+ await new Promise(() => {
3860
+ });
3861
+ });
3689
3862
  program.command("da-status").description("Check Nexus backend status").action(async () => {
3690
3863
  const backendUrl = process.env.BACKEND_URL || "http://localhost:4200";
3691
3864
  try {
@@ -3703,12 +3876,20 @@ program.command("da-status").description("Check Nexus backend status").action(as
3703
3876
  console.log(" cd ~/Projects/nexus && python -m src.deep_agents_server");
3704
3877
  }
3705
3878
  });
3706
- for (const cmd of cli.commands) {
3707
- const name = cmd.name();
3708
- if (!["da-init", "run", "dashboard", "da-status"].includes(name)) {
3709
- program.addCommand(cmd);
3710
- }
3711
- }
3879
+ program.addCommand(installCommand);
3880
+ program.addCommand(initCommand);
3881
+ program.addCommand(startCommand);
3882
+ program.addCommand(stopCommand);
3883
+ program.addCommand(statusCommand);
3884
+ program.addCommand(doctorCommand);
3885
+ program.addCommand(logsCommand);
3886
+ program.addCommand(updateCommand);
3887
+ program.addCommand(destroyCommand);
3888
+ program.addCommand(keysCommand);
3889
+ program.addCommand(sshCommand);
3890
+ program.addCommand(brainstormCommand);
3891
+ program.addCommand(ninetyNineCommand);
3892
+ program.addCommand(shellCommand2);
3712
3893
  if (!process.argv.slice(2).length) {
3713
3894
  interactiveMode().catch((err) => {
3714
3895
  console.error(err);
@@ -3717,3 +3898,6 @@ if (!process.argv.slice(2).length) {
3717
3898
  } else {
3718
3899
  program.parse();
3719
3900
  }
3901
+ export {
3902
+ version
3903
+ };