buildwithnexus 0.7.2 → 0.7.4

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
@@ -428,6 +428,112 @@ var init_platform = __esm({
428
428
  }
429
429
  });
430
430
 
431
+ // src/core/utils.ts
432
+ function backoffMs(attempt) {
433
+ return Math.min(3e3 * Math.pow(2, attempt), 3e4);
434
+ }
435
+ var init_utils = __esm({
436
+ "src/core/utils.ts"() {
437
+ "use strict";
438
+ }
439
+ });
440
+
441
+ // src/core/health.ts
442
+ var health_exports = {};
443
+ __export(health_exports, {
444
+ checkHealth: () => checkHealth,
445
+ waitForServer: () => waitForServer
446
+ });
447
+ async function checkHealth(vmRunning) {
448
+ const status = {
449
+ vmRunning,
450
+ sshReady: false,
451
+ dockerReady: false,
452
+ serverHealthy: false,
453
+ tunnelUrl: null,
454
+ dockerVersion: null,
455
+ serverVersion: null,
456
+ diskUsagePercent: null,
457
+ uptimeSeconds: null,
458
+ lastChecked: (/* @__PURE__ */ new Date()).toISOString()
459
+ };
460
+ if (!vmRunning) return status;
461
+ status.sshReady = true;
462
+ try {
463
+ const { stdout, code } = await dockerExec("docker version --format '{{.Server.Version}}'");
464
+ status.dockerReady = code === 0 && stdout.trim().length > 0;
465
+ if (status.dockerReady) status.dockerVersion = stdout.trim();
466
+ } catch {
467
+ }
468
+ try {
469
+ const { stdout, code } = await dockerExec("curl -sf http://localhost:4200/health");
470
+ status.serverHealthy = code === 0 && stdout.includes("ok");
471
+ if (status.serverHealthy) {
472
+ try {
473
+ const parsed = JSON.parse(stdout);
474
+ if (typeof parsed.version === "string") status.serverVersion = parsed.version;
475
+ } catch {
476
+ }
477
+ }
478
+ } catch {
479
+ }
480
+ try {
481
+ const { stdout } = await dockerExec("df / --output=pcent | tail -1 | tr -dc '0-9'");
482
+ const pct = parseInt(stdout.trim(), 10);
483
+ if (!isNaN(pct)) status.diskUsagePercent = pct;
484
+ } catch {
485
+ }
486
+ try {
487
+ const { stdout } = await dockerExec("awk '{print int($1)}' /proc/uptime 2>/dev/null");
488
+ const up = parseInt(stdout.trim(), 10);
489
+ if (!isNaN(up)) status.uptimeSeconds = up;
490
+ } catch {
491
+ }
492
+ try {
493
+ const { stdout } = await dockerExec("cat /home/nexus/.nexus/tunnel-url.txt 2>/dev/null");
494
+ if (stdout.includes("https://")) {
495
+ status.tunnelUrl = stdout.trim();
496
+ }
497
+ } catch {
498
+ }
499
+ return status;
500
+ }
501
+ async function waitForServer(timeoutMs = 9e5) {
502
+ const start = Date.now();
503
+ let lastLog = 0;
504
+ let attempt = 0;
505
+ while (Date.now() - start < timeoutMs) {
506
+ try {
507
+ const { stdout, code } = await dockerExec("curl -sf http://localhost:4200/health");
508
+ if (code === 0 && stdout.includes("ok")) return true;
509
+ } catch {
510
+ }
511
+ const elapsed = Date.now() - start;
512
+ if (elapsed - lastLog >= 3e4) {
513
+ lastLog = elapsed;
514
+ try {
515
+ const { stdout } = await dockerExec("systemctl is-active nexus 2>/dev/null || echo 'starting...'");
516
+ process.stderr.write(`
517
+ [server ${Math.round(elapsed / 1e3)}s] ${stdout.trim().slice(0, 120)}
518
+ `);
519
+ } catch {
520
+ }
521
+ }
522
+ const delay = backoffMs(attempt++);
523
+ const remaining = timeoutMs - (Date.now() - start);
524
+ if (remaining <= 0) break;
525
+ await new Promise((r) => setTimeout(r, Math.min(delay, remaining)));
526
+ }
527
+ return false;
528
+ }
529
+ var init_health = __esm({
530
+ "src/core/health.ts"() {
531
+ "use strict";
532
+ init_docker();
533
+ init_utils();
534
+ }
535
+ });
536
+
431
537
  // src/core/docker.ts
432
538
  var docker_exports = {};
433
539
  __export(docker_exports, {
@@ -436,6 +542,7 @@ __export(docker_exports, {
436
542
  installDocker: () => installDocker,
437
543
  isDockerInstalled: () => isDockerInstalled,
438
544
  isNexusRunning: () => isNexusRunning,
545
+ launchNexus: () => launchNexus,
439
546
  pullImage: () => pullImage,
440
547
  startBackend: () => startBackend,
441
548
  startNexus: () => startNexus,
@@ -811,7 +918,7 @@ async function startBackend() {
811
918
  const path11 = await import("path");
812
919
  const nexusDir = path11.join(os5.homedir(), "Projects", "nexus");
813
920
  log.step(`Starting Nexus backend from ${nexusDir}...`);
814
- const child = spawn2("python", ["-m", "src.deep_agents_server"], {
921
+ const child = spawn2("python3", ["-m", "src.deep_agents_server"], {
815
922
  cwd: nexusDir,
816
923
  detached: true,
817
924
  stdio: "ignore",
@@ -820,6 +927,16 @@ async function startBackend() {
820
927
  child.unref();
821
928
  log.success("Nexus backend process started");
822
929
  }
930
+ async function launchNexus(keys, config, opts) {
931
+ const { healthTimeoutMs = 6e4, stopExisting = true } = opts ?? {};
932
+ if (stopExisting && await isNexusRunning()) {
933
+ await stopNexus();
934
+ }
935
+ await startNexus(keys, config);
936
+ if (healthTimeoutMs <= 0) return true;
937
+ const { waitForServer: waitForServer2 } = await Promise.resolve().then(() => (init_health(), health_exports));
938
+ return waitForServer2(healthTimeoutMs);
939
+ }
823
940
  async function isNexusRunning() {
824
941
  try {
825
942
  const { stdout } = await execa("docker", [
@@ -1228,6 +1345,77 @@ function validateBackendUrl(url) {
1228
1345
  };
1229
1346
  }
1230
1347
 
1348
+ // src/core/api.ts
1349
+ function buildRunPayload(task, agentRole, agentGoal, keys) {
1350
+ const k = keys ?? loadApiKeys();
1351
+ return {
1352
+ task,
1353
+ agent_role: agentRole,
1354
+ agent_goal: agentGoal,
1355
+ api_key: k.anthropic || "",
1356
+ openai_api_key: k.openai || "",
1357
+ google_api_key: k.google || ""
1358
+ };
1359
+ }
1360
+ async function httpPost(httpPort, path11, body, timeoutMs = 6e4) {
1361
+ const res = await fetch(`http://localhost:${httpPort}${path11}`, {
1362
+ method: "POST",
1363
+ headers: { "Content-Type": "application/json" },
1364
+ body: JSON.stringify(body),
1365
+ signal: AbortSignal.timeout(timeoutMs)
1366
+ });
1367
+ if (!res.ok) throw new Error(`Server returned ${res.status}`);
1368
+ const text = await res.text();
1369
+ try {
1370
+ const parsed = JSON.parse(text);
1371
+ return parsed.response ?? parsed.message ?? text;
1372
+ } catch {
1373
+ return text;
1374
+ }
1375
+ }
1376
+ async function httpGet(httpPort, path11, timeoutMs = 1e4) {
1377
+ try {
1378
+ const res = await fetch(`http://localhost:${httpPort}${path11}`, {
1379
+ signal: AbortSignal.timeout(timeoutMs)
1380
+ });
1381
+ const text = await res.text();
1382
+ return { ok: res.ok, text };
1383
+ } catch {
1384
+ return { ok: false, text: "" };
1385
+ }
1386
+ }
1387
+ async function checkServerHealth(target, timeoutMs = 1e4) {
1388
+ const url = typeof target === "number" ? `http://localhost:${target}/health` : `${target}/health`;
1389
+ try {
1390
+ const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
1391
+ return res.ok;
1392
+ } catch {
1393
+ return false;
1394
+ }
1395
+ }
1396
+
1397
+ // src/core/sse-parser.ts
1398
+ async function* parseSSEStream(reader) {
1399
+ const decoder = new TextDecoder();
1400
+ let buffer = "";
1401
+ while (true) {
1402
+ const { done, value } = await reader.read();
1403
+ if (done) break;
1404
+ buffer += decoder.decode(value, { stream: true });
1405
+ const lines = buffer.split("\n");
1406
+ buffer = lines.pop() || "";
1407
+ for (const line of lines) {
1408
+ if (!line.startsWith("data: ")) continue;
1409
+ try {
1410
+ const parsed = JSON.parse(line.slice(6));
1411
+ yield parsed;
1412
+ } catch (e) {
1413
+ if (process.env.LOG_LEVEL === "debug") console.error("SSE parse error:", e);
1414
+ }
1415
+ }
1416
+ }
1417
+ }
1418
+
1231
1419
  // src/cli/run-command.ts
1232
1420
  async function runCommand(task, options) {
1233
1421
  const backendUrl = process.env.BACKEND_URL || "http://localhost:4200";
@@ -1240,30 +1428,17 @@ ${urlCheck.error}`);
1240
1428
  tui.displayHeader(task, options.agent);
1241
1429
  tui.displayConnecting();
1242
1430
  try {
1243
- let healthOk = false;
1244
- try {
1245
- const healthResponse = await fetch(`${backendUrl}/health`);
1246
- healthOk = healthResponse.ok;
1247
- } catch {
1248
- }
1249
- if (!healthOk) {
1431
+ if (!await checkServerHealth(backendUrl)) {
1250
1432
  console.error(
1251
1433
  "Backend not responding. Start it with:\n buildwithnexus server"
1252
1434
  );
1253
1435
  process.exit(1);
1254
1436
  }
1255
- const keys = loadApiKeys();
1437
+ const payload = buildRunPayload(task, options.agent, options.goal || "");
1256
1438
  const response = await fetch(`${backendUrl}/api/run`, {
1257
1439
  method: "POST",
1258
1440
  headers: { "Content-Type": "application/json" },
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
- })
1441
+ body: JSON.stringify(payload)
1267
1442
  });
1268
1443
  if (!response.ok) {
1269
1444
  console.error("Backend error");
@@ -1415,32 +1590,13 @@ function classifyIntent(task) {
1415
1590
 
1416
1591
  // src/cli/interactive.ts
1417
1592
  init_secrets();
1593
+ init_docker();
1418
1594
 
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;
1431
- try {
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);
1436
- }
1437
- }
1438
- }
1439
- }
1595
+ // src/core/version.ts
1596
+ var resolvedVersion = true ? "0.7.4" : pkg.version;
1440
1597
 
1441
1598
  // src/cli/interactive.ts
1442
- init_docker();
1443
- var appVersion = true ? "0.7.2" : pkg.version;
1599
+ var appVersion = resolvedVersion;
1444
1600
  async function interactiveMode() {
1445
1601
  const backendUrl = process.env.BACKEND_URL || "http://localhost:4200";
1446
1602
  const urlCheck = validateBackendUrl(backendUrl);
@@ -1479,26 +1635,11 @@ ${urlCheck.error}`));
1479
1635
  async function waitForBackend() {
1480
1636
  for (let i = 0; i < 5; i++) {
1481
1637
  await new Promise((resolve) => setTimeout(resolve, 2e3));
1482
- try {
1483
- const retryResponse = await fetch(`${backendUrl}/health`, { signal: AbortSignal.timeout(1e4) });
1484
- if (retryResponse.ok) return true;
1485
- } catch {
1486
- }
1638
+ if (await checkServerHealth(backendUrl)) return true;
1487
1639
  }
1488
1640
  return false;
1489
1641
  }
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);
1499
- }
1500
- }
1501
- } catch {
1642
+ if (!await checkServerHealth(backendUrl)) {
1502
1643
  console.log(chalk3.yellow("\u26A0\uFE0F Backend not accessible, attempting to start..."));
1503
1644
  await startBackend();
1504
1645
  const ready = await waitForBackend();
@@ -1619,25 +1760,45 @@ async function planModeLoop(task, backendUrl, ask) {
1619
1760
  console.log("");
1620
1761
  console.log(chalk3.yellow("\u23F3 Fetching plan from backend..."));
1621
1762
  let steps = [];
1622
- const keys = loadApiKeys();
1623
1763
  try {
1624
1764
  const response = await fetch(`${backendUrl}/api/run`, {
1625
1765
  method: "POST",
1626
1766
  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 || "" }),
1767
+ body: JSON.stringify(buildRunPayload(task, "engineer", "")),
1628
1768
  signal: AbortSignal.timeout(12e4)
1629
1769
  });
1630
1770
  if (!response.ok) {
1631
1771
  console.error(chalk3.red("Backend error \u2014 cannot fetch plan."));
1632
1772
  return "cancel";
1633
1773
  }
1634
- const { run_id } = await response.json();
1774
+ const planText = await response.text();
1775
+ let planParsed;
1776
+ try {
1777
+ planParsed = JSON.parse(planText);
1778
+ } catch {
1779
+ console.error(chalk3.red(`Backend returned invalid JSON: ${planText.slice(0, 200)}`));
1780
+ return "cancel";
1781
+ }
1782
+ const { run_id: planRunId } = planParsed;
1783
+ if (!planRunId || typeof planRunId !== "string") {
1784
+ console.error(chalk3.red("Backend did not return a valid run ID"));
1785
+ return "cancel";
1786
+ }
1787
+ if (!/^[a-zA-Z0-9_-]+$/.test(planRunId)) {
1788
+ console.error(chalk3.red("Backend returned run ID with invalid characters"));
1789
+ return "cancel";
1790
+ }
1791
+ const run_id = planRunId;
1635
1792
  tui.displayConnected(run_id);
1636
1793
  const streamResponse = await fetch(`${backendUrl}/api/stream/${run_id}`, { signal: AbortSignal.timeout(12e4) });
1794
+ if (!streamResponse.ok) {
1795
+ console.error(chalk3.red(`Stream endpoint returned ${streamResponse.status}`));
1796
+ return "cancel";
1797
+ }
1637
1798
  const reader = streamResponse.body?.getReader();
1638
1799
  if (!reader) throw new Error("No response body");
1639
1800
  let planReceived = false;
1640
- for await (const parsed of parseSSEStream2(reader)) {
1801
+ for await (const parsed of parseSSEStream(reader)) {
1641
1802
  if (parsed.type === "plan") {
1642
1803
  steps = parsed.data["steps"] || [];
1643
1804
  planReceived = true;
@@ -1710,26 +1871,46 @@ async function editPlanSteps(steps, ask) {
1710
1871
  async function buildModeLoop(task, backendUrl, ask) {
1711
1872
  console.log(chalk3.bold("Task:"), chalk3.white(task));
1712
1873
  tui.displayConnecting();
1713
- const keys = loadApiKeys();
1714
1874
  try {
1715
1875
  const response = await fetch(`${backendUrl}/api/run`, {
1716
1876
  method: "POST",
1717
1877
  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 || "" }),
1878
+ body: JSON.stringify(buildRunPayload(task, "engineer", "")),
1719
1879
  signal: AbortSignal.timeout(12e4)
1720
1880
  });
1721
1881
  if (!response.ok) {
1722
1882
  console.error(chalk3.red("Backend error"));
1723
1883
  return "done";
1724
1884
  }
1725
- const { run_id } = await response.json();
1885
+ const buildText = await response.text();
1886
+ let buildParsed;
1887
+ try {
1888
+ buildParsed = JSON.parse(buildText);
1889
+ } catch {
1890
+ console.error(chalk3.red(`Backend returned invalid JSON: ${buildText.slice(0, 200)}`));
1891
+ return "done";
1892
+ }
1893
+ const { run_id: buildRunId } = buildParsed;
1894
+ if (!buildRunId || typeof buildRunId !== "string") {
1895
+ console.error(chalk3.red("Backend did not return a valid run ID"));
1896
+ return "done";
1897
+ }
1898
+ if (!/^[a-zA-Z0-9_-]+$/.test(buildRunId)) {
1899
+ console.error(chalk3.red("Backend returned run ID with invalid characters"));
1900
+ return "done";
1901
+ }
1902
+ const run_id = buildRunId;
1726
1903
  tui.displayConnected(run_id);
1727
1904
  console.log(chalk3.bold.green("\u2699\uFE0F Executing..."));
1728
1905
  tui.displayStreamStart();
1729
1906
  const streamResponse = await fetch(`${backendUrl}/api/stream/${run_id}`, { signal: AbortSignal.timeout(12e4) });
1907
+ if (!streamResponse.ok) {
1908
+ console.error(chalk3.red(`Stream endpoint returned ${streamResponse.status}`));
1909
+ return "done";
1910
+ }
1730
1911
  const reader = streamResponse.body?.getReader();
1731
1912
  if (!reader) throw new Error("No response body");
1732
- for await (const parsed of parseSSEStream2(reader)) {
1913
+ for await (const parsed of parseSSEStream(reader)) {
1733
1914
  const type = parsed.type;
1734
1915
  if (type === "execution_complete") {
1735
1916
  const summary = parsed.data["summary"] || "";
@@ -1768,58 +1949,77 @@ async function brainstormModeLoop(task, backendUrl, ask) {
1768
1949
  while (true) {
1769
1950
  console.log(chalk3.bold.blue("\u{1F4A1} Thinking..."));
1770
1951
  try {
1771
- const keys = loadApiKeys();
1772
1952
  const response = await fetch(`${backendUrl}/api/run`, {
1773
1953
  method: "POST",
1774
1954
  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
- }),
1955
+ body: JSON.stringify(buildRunPayload(
1956
+ currentQuestion,
1957
+ "brainstorm",
1958
+ "Generate ideas, considerations, and suggestions. Be concise and helpful."
1959
+ )),
1783
1960
  signal: AbortSignal.timeout(12e4)
1784
1961
  });
1785
1962
  if (response.ok) {
1786
- const { run_id } = await response.json();
1963
+ const brainstormText = await response.text();
1964
+ let brainstormParsed;
1965
+ try {
1966
+ brainstormParsed = JSON.parse(brainstormText);
1967
+ } catch {
1968
+ console.error(chalk3.red(`Backend returned invalid JSON: ${brainstormText.slice(0, 200)}`));
1969
+ continue;
1970
+ }
1971
+ const { run_id: brainstormRunId } = brainstormParsed;
1972
+ if (!brainstormRunId || typeof brainstormRunId !== "string") {
1973
+ console.error(chalk3.red("Backend did not return a valid run ID"));
1974
+ continue;
1975
+ }
1976
+ if (!/^[a-zA-Z0-9_-]+$/.test(brainstormRunId)) {
1977
+ console.error(chalk3.red("Backend returned run ID with invalid characters"));
1978
+ continue;
1979
+ }
1980
+ const run_id = brainstormRunId;
1787
1981
  const streamResponse = await fetch(`${backendUrl}/api/stream/${run_id}`, { signal: AbortSignal.timeout(12e4) });
1982
+ if (!streamResponse.ok) {
1983
+ console.error(chalk3.red(`Stream endpoint returned ${streamResponse.status}`));
1984
+ continue;
1985
+ }
1788
1986
  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)"));
1987
+ if (!reader) {
1988
+ console.error(chalk3.red("No response body from agent"));
1989
+ continue;
1990
+ }
1991
+ let responseText = "";
1992
+ for await (const parsed of parseSSEStream(reader)) {
1993
+ const type = parsed.type;
1994
+ const data = parsed.data;
1995
+ if (type === "done" || type === "execution_complete" || type === "final_result") {
1996
+ const summary = data["summary"] || data["result"] || "";
1997
+ if (summary) responseText = summary;
1998
+ break;
1999
+ } else if (type === "error") {
2000
+ const errorMsg = data["error"] || data["content"] || "Unknown error";
2001
+ responseText += errorMsg + "\n";
2002
+ break;
2003
+ } else if (type === "thought" || type === "observation") {
2004
+ const content = data["content"] || "";
2005
+ if (content) responseText += content + "\n";
2006
+ } else if (type === "agent_response" || type === "agent_result") {
2007
+ const content = data["content"] || data["result"] || "";
2008
+ if (content) responseText += content + "\n";
2009
+ } else if (type === "action") {
2010
+ const content = data["content"] || "";
2011
+ if (content) responseText += content + "\n";
2012
+ } else if (type === "agent_working" || type === "started") {
2013
+ } else if (type !== "plan") {
2014
+ const content = data["content"] || data["response"] || "";
2015
+ if (content) responseText += content + "\n";
1821
2016
  }
1822
2017
  }
2018
+ if (responseText.trim()) {
2019
+ tui.displayBrainstormResponse(responseText.trim());
2020
+ } else {
2021
+ console.log(chalk3.gray("(No response received from agent)"));
2022
+ }
1823
2023
  } else {
1824
2024
  console.log(chalk3.red("Could not reach backend for brainstorm response."));
1825
2025
  }
@@ -2021,26 +2221,6 @@ async function withSpinner(spinner, label, fn) {
2021
2221
  succeed(spinner, label);
2022
2222
  return result;
2023
2223
  }
2024
- async function waitForHealthy(port, timeoutMs = 12e4) {
2025
- const start = Date.now();
2026
- let attempt = 0;
2027
- const backoffMs = (n) => Math.min(2e3 * Math.pow(2, n), 1e4);
2028
- while (Date.now() - start < timeoutMs) {
2029
- try {
2030
- const res = await fetch(`http://localhost:${port}/health`);
2031
- if (res.ok) {
2032
- const body = await res.text();
2033
- if (body.includes("ok")) return true;
2034
- }
2035
- } catch {
2036
- }
2037
- const delay = backoffMs(attempt++);
2038
- const remaining = timeoutMs - (Date.now() - start);
2039
- if (remaining <= 0) break;
2040
- await new Promise((r) => setTimeout(r, Math.min(delay, remaining)));
2041
- }
2042
- return false;
2043
- }
2044
2224
  var phases = [
2045
2225
  // Phase 1 — Configuration (~30s)
2046
2226
  {
@@ -2120,19 +2300,16 @@ var phases = [
2120
2300
  name: "Launch",
2121
2301
  run: async (ctx, spinner) => {
2122
2302
  const { config, keys } = ctx;
2123
- const alreadyRunning = await isNexusRunning();
2124
- if (alreadyRunning) {
2125
- await withSpinner(spinner, "Stopping existing NEXUS container...", () => stopNexus());
2126
- }
2127
2303
  await withSpinner(
2128
2304
  spinner,
2129
2305
  "Starting NEXUS container...",
2130
- () => startNexus(
2306
+ () => launchNexus(
2131
2307
  {
2132
2308
  anthropic: keys.ANTHROPIC_API_KEY,
2133
2309
  openai: keys.OPENAI_API_KEY || ""
2134
2310
  },
2135
- { port: config.httpPort }
2311
+ { port: config.httpPort },
2312
+ { healthTimeoutMs: 0, stopExisting: true }
2136
2313
  )
2137
2314
  );
2138
2315
  ctx.containerStarted = true;
@@ -2145,7 +2322,8 @@ var phases = [
2145
2322
  const { config } = ctx;
2146
2323
  spinner.text = `Waiting for NEXUS server on port ${config.httpPort}...`;
2147
2324
  spinner.start();
2148
- const healthy = await waitForHealthy(config.httpPort);
2325
+ const { waitForServer: waitForServer2 } = await Promise.resolve().then(() => (init_health(), health_exports));
2326
+ const healthy = await waitForServer2(12e4);
2149
2327
  if (!healthy) {
2150
2328
  fail(spinner, "Server failed to start within 120s");
2151
2329
  log.warn("Check logs: docker logs nexus");
@@ -2167,11 +2345,11 @@ var phases = [
2167
2345
  await withSpinner(
2168
2346
  spinner,
2169
2347
  "Installing cloudflared...",
2170
- () => installCloudflared(config.httpPort, platform.arch)
2348
+ () => installCloudflared(platform.arch)
2171
2349
  );
2172
2350
  spinner.text = "Starting tunnel...";
2173
2351
  spinner.start();
2174
- const url = await startTunnel(config.httpPort);
2352
+ const url = await startTunnel();
2175
2353
  if (url) {
2176
2354
  ctx.tunnelUrl = url;
2177
2355
  succeed(spinner, `Tunnel active: ${url}`);
@@ -2231,40 +2409,6 @@ import { Command as Command3 } from "commander";
2231
2409
  init_logger();
2232
2410
  init_secrets();
2233
2411
  init_docker();
2234
-
2235
- // src/core/health.ts
2236
- init_docker();
2237
- async function waitForServer(timeoutMs = 9e5) {
2238
- const start = Date.now();
2239
- let lastLog = 0;
2240
- let attempt = 0;
2241
- const backoffMs = (n) => Math.min(3e3 * Math.pow(2, n), 3e4);
2242
- while (Date.now() - start < timeoutMs) {
2243
- try {
2244
- const { stdout, code } = await dockerExec("curl -sf http://localhost:4200/health");
2245
- if (code === 0 && stdout.includes("ok")) return true;
2246
- } catch {
2247
- }
2248
- const elapsed = Date.now() - start;
2249
- if (elapsed - lastLog >= 3e4) {
2250
- lastLog = elapsed;
2251
- try {
2252
- const { stdout } = await dockerExec("systemctl is-active nexus 2>/dev/null || echo 'starting...'");
2253
- process.stderr.write(`
2254
- [server ${Math.round(elapsed / 1e3)}s] ${stdout.trim().slice(0, 120)}
2255
- `);
2256
- } catch {
2257
- }
2258
- }
2259
- const delay = backoffMs(attempt++);
2260
- const remaining = timeoutMs - (Date.now() - start);
2261
- if (remaining <= 0) break;
2262
- await new Promise((r) => setTimeout(r, Math.min(delay, remaining)));
2263
- }
2264
- return false;
2265
- }
2266
-
2267
- // src/commands/start.ts
2268
2412
  var startCommand = new Command3("start").description("Start the NEXUS runtime").action(async () => {
2269
2413
  const config = loadConfig();
2270
2414
  if (!config) {
@@ -2275,25 +2419,25 @@ var startCommand = new Command3("start").description("Start the NEXUS runtime").
2275
2419
  log.success("NEXUS is already running");
2276
2420
  return;
2277
2421
  }
2278
- let spinner = createSpinner("Pulling NEXUS image...");
2422
+ let spinner = createSpinner("Checking NEXUS image...");
2279
2423
  spinner.start();
2280
- await pullImage("buildwithnexus/nexus", "latest");
2424
+ const localExists = await imageExistsLocally("buildwithnexus/nexus", "latest");
2425
+ if (!localExists) {
2426
+ spinner.text = "Pulling NEXUS image...";
2427
+ await pullImage("buildwithnexus/nexus", "latest");
2428
+ }
2281
2429
  succeed(spinner, "Image ready");
2282
- spinner = createSpinner("Starting NEXUS container...");
2283
- spinner.start();
2284
2430
  const keys = loadKeys();
2285
2431
  if (!keys) {
2286
- fail(spinner, "No API keys found. Run: buildwithnexus init");
2432
+ log.error("No API keys found. Run: buildwithnexus init");
2287
2433
  process.exit(1);
2288
2434
  }
2289
- await startNexus(
2435
+ spinner = createSpinner("Starting NEXUS container...");
2436
+ spinner.start();
2437
+ const ok = await launchNexus(
2290
2438
  { anthropic: keys.ANTHROPIC_API_KEY, openai: keys.OPENAI_API_KEY || "" },
2291
2439
  { port: config.httpPort }
2292
2440
  );
2293
- succeed(spinner, "Container started");
2294
- spinner = createSpinner("Waiting for NEXUS server...");
2295
- spinner.start();
2296
- const ok = await waitForServer(6e4);
2297
2441
  if (ok) {
2298
2442
  succeed(spinner, "NEXUS server running");
2299
2443
  } else {
@@ -2314,13 +2458,11 @@ var startCommand = new Command3("start").description("Start the NEXUS runtime").
2314
2458
 
2315
2459
  // src/commands/stop.ts
2316
2460
  import { Command as Command4 } from "commander";
2461
+ import { execa as execa3 } from "execa";
2317
2462
  init_logger();
2318
- import { execFile } from "child_process";
2319
- import { promisify } from "util";
2320
- var execFileAsync = promisify(execFile);
2321
2463
  async function containerExists() {
2322
2464
  try {
2323
- const { stdout } = await execFileAsync("docker", [
2465
+ const { stdout } = await execa3("docker", [
2324
2466
  "ps",
2325
2467
  "-a",
2326
2468
  "--filter",
@@ -2335,7 +2477,7 @@ async function containerExists() {
2335
2477
  }
2336
2478
  async function isContainerRunning() {
2337
2479
  try {
2338
- const { stdout } = await execFileAsync("docker", [
2480
+ const { stdout } = await execa3("docker", [
2339
2481
  "ps",
2340
2482
  "--filter",
2341
2483
  "name=^nexus$",
@@ -2359,10 +2501,10 @@ var stopCommand = new Command4("stop").description("Gracefully shut down the NEX
2359
2501
  try {
2360
2502
  if (await isContainerRunning()) {
2361
2503
  spinner.text = "Stopping container...";
2362
- await execFileAsync("docker", ["stop", "nexus"]);
2504
+ await execa3("docker", ["stop", "nexus"]);
2363
2505
  }
2364
2506
  spinner.text = "Removing container...";
2365
- await execFileAsync("docker", ["rm", "nexus"]);
2507
+ await execa3("docker", ["rm", "nexus"]);
2366
2508
  succeed(spinner, "NEXUS container stopped and removed");
2367
2509
  } catch (err) {
2368
2510
  fail(spinner, "Failed to stop NEXUS container");
@@ -2506,11 +2648,11 @@ var doctorCommand = new Command6("doctor").description("Diagnose NEXUS runtime e
2506
2648
  // src/commands/logs.ts
2507
2649
  init_logger();
2508
2650
  import { Command as Command7 } from "commander";
2509
- import { execa as execa3 } from "execa";
2651
+ import { execa as execa4 } from "execa";
2510
2652
  var logsCommand = new Command7("logs").description("View NEXUS server logs").action(async () => {
2511
2653
  let containerExists2 = false;
2512
2654
  try {
2513
- const { stdout } = await execa3("docker", [
2655
+ const { stdout } = await execa4("docker", [
2514
2656
  "ps",
2515
2657
  "-a",
2516
2658
  "--filter",
@@ -2527,7 +2669,7 @@ var logsCommand = new Command7("logs").description("View NEXUS server logs").act
2527
2669
  log.error("NEXUS container not found. Start with: buildwithnexus start");
2528
2670
  process.exit(1);
2529
2671
  }
2530
- const proc = execa3("docker", ["logs", "-f", "nexus"], {
2672
+ const proc = execa4("docker", ["logs", "-f", "nexus"], {
2531
2673
  stdout: "pipe",
2532
2674
  stderr: "pipe"
2533
2675
  });
@@ -2544,7 +2686,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
2544
2686
  init_logger();
2545
2687
  init_secrets();
2546
2688
  init_docker();
2547
- import { execa as execa4 } from "execa";
2689
+ import { execa as execa5 } from "execa";
2548
2690
  function getReleaseTarball() {
2549
2691
  const dir = path6.dirname(fileURLToPath2(import.meta.url));
2550
2692
  const tarballPath = path6.join(dir, "nexus-release.tar.gz");
@@ -2566,7 +2708,7 @@ var updateCommand = new Command8("update").description("Update NEXUS to the late
2566
2708
  let spinner = createSpinner("Uploading release tarball...");
2567
2709
  spinner.start();
2568
2710
  const tarballPath = getReleaseTarball();
2569
- await execa4("docker", ["cp", tarballPath, "nexus:/tmp/nexus-release.tar.gz"]);
2711
+ await execa5("docker", ["cp", tarballPath, "nexus:/tmp/nexus-release.tar.gz"]);
2570
2712
  succeed(spinner, "Tarball uploaded");
2571
2713
  spinner = createSpinner("Stopping NEXUS server...");
2572
2714
  spinner.start();
@@ -2720,7 +2862,7 @@ keysCommand.command("set <key>").description("Set or update an API key (e.g. ANT
2720
2862
  init_logger();
2721
2863
  init_docker();
2722
2864
  import { Command as Command11 } from "commander";
2723
- import { execa as execa5 } from "execa";
2865
+ import { execa as execa6 } from "execa";
2724
2866
  var sshCommand = new Command11("ssh").description("Open an interactive shell inside the NEXUS container").action(async () => {
2725
2867
  const running = await isNexusRunning();
2726
2868
  if (!running) {
@@ -2728,7 +2870,7 @@ var sshCommand = new Command11("ssh").description("Open an interactive shell ins
2728
2870
  process.exit(1);
2729
2871
  }
2730
2872
  log.dim("Opening shell in NEXUS container...");
2731
- await execa5("docker", ["exec", "-it", "nexus", "/bin/bash"], { stdio: "inherit" });
2873
+ await execa6("docker", ["exec", "-it", "nexus", "/bin/bash"], { stdio: "inherit" });
2732
2874
  });
2733
2875
 
2734
2876
  // src/commands/brainstorm.ts
@@ -2776,13 +2918,7 @@ var brainstormCommand = new Command12("brainstorm").description("Brainstorm an i
2776
2918
  }
2777
2919
  const spinner = createSpinner("Connecting to NEXUS...");
2778
2920
  spinner.start();
2779
- let healthOk = false;
2780
- try {
2781
- const healthRes = await fetch(`http://localhost:${config.httpPort}/health`);
2782
- const healthText = await healthRes.text();
2783
- healthOk = healthRes.ok && healthText.includes("ok");
2784
- } catch {
2785
- }
2921
+ const healthOk = await checkServerHealth(config.httpPort);
2786
2922
  if (!healthOk) {
2787
2923
  fail(spinner, "NEXUS server is not healthy");
2788
2924
  log.warn("Check status: buildwithnexus status");
@@ -3303,33 +3439,6 @@ var EventStream = class {
3303
3439
  };
3304
3440
 
3305
3441
  // src/commands/shell.ts
3306
- async function httpPost(httpPort, path11, body) {
3307
- const res = await fetch(`http://localhost:${httpPort}${path11}`, {
3308
- method: "POST",
3309
- headers: { "Content-Type": "application/json" },
3310
- body: JSON.stringify(body),
3311
- signal: AbortSignal.timeout(6e4)
3312
- });
3313
- if (!res.ok) throw new Error(`Server returned ${res.status}`);
3314
- const text = await res.text();
3315
- try {
3316
- const parsed = JSON.parse(text);
3317
- return parsed.response ?? parsed.message ?? text;
3318
- } catch {
3319
- return text;
3320
- }
3321
- }
3322
- async function httpGet(httpPort, path11) {
3323
- try {
3324
- const res = await fetch(`http://localhost:${httpPort}${path11}`, {
3325
- signal: AbortSignal.timeout(1e4)
3326
- });
3327
- const text = await res.text();
3328
- return { ok: res.ok, text };
3329
- } catch {
3330
- return { ok: false, text: "" };
3331
- }
3332
- }
3333
3442
  async function sendMessage2(httpPort, message) {
3334
3443
  return httpPost(httpPort, "/message", { message, source: "shell" });
3335
3444
  }
@@ -3563,12 +3672,12 @@ var shellCommand2 = new Command14("shell").description("Launch the interactive N
3563
3672
  name: "logs",
3564
3673
  description: "Show recent container logs",
3565
3674
  handler: async () => {
3566
- const { execa: execa6 } = await import("execa");
3675
+ const { execa: execa7 } = await import("execa");
3567
3676
  console.log("");
3568
3677
  console.log(chalk16.bold(" Recent Logs:"));
3569
3678
  console.log(chalk16.dim(" \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"));
3570
3679
  try {
3571
- const { stdout } = await execa6("docker", ["logs", "--tail", "30", "nexus"]);
3680
+ const { stdout } = await execa7("docker", ["logs", "--tail", "30", "nexus"]);
3572
3681
  for (const line of redact(stdout).split("\n")) {
3573
3682
  console.log(chalk16.dim(" " + line));
3574
3683
  }
@@ -3582,10 +3691,10 @@ var shellCommand2 = new Command14("shell").description("Launch the interactive N
3582
3691
  name: "exec",
3583
3692
  description: "Drop into the container shell for debugging/inspection",
3584
3693
  handler: async () => {
3585
- const { execa: execa6 } = await import("execa");
3694
+ const { execa: execa7 } = await import("execa");
3586
3695
  eventStream.stop();
3587
3696
  try {
3588
- await execa6("docker", ["exec", "-it", "nexus", "/bin/sh"], { stdio: "inherit" });
3697
+ await execa7("docker", ["exec", "-it", "nexus", "/bin/sh"], { stdio: "inherit" });
3589
3698
  } catch {
3590
3699
  }
3591
3700
  eventStream.start();
@@ -3845,7 +3954,7 @@ import os4 from "os";
3845
3954
  import path10 from "path";
3846
3955
  var homeEnvPath = path10.join(os4.homedir(), ".env.local");
3847
3956
  dotenv2.config({ path: homeEnvPath });
3848
- var version = true ? "0.7.2" : pkg.version;
3957
+ var version = resolvedVersion;
3849
3958
  checkForUpdates(version);
3850
3959
  program.name("buildwithnexus").description("Nexus - AI-Powered Task Execution").version(version);
3851
3960
  program.command("da-init").description("Initialize Nexus (set up API keys and .env.local)").action(deepAgentsInitCommand);
@@ -231,6 +231,19 @@ function loadApiKeys() {
231
231
  return result;
232
232
  }
233
233
 
234
+ // src/core/api.ts
235
+ function buildRunPayload(task, agentRole, agentGoal, keys) {
236
+ const k = keys ?? loadApiKeys();
237
+ return {
238
+ task,
239
+ agent_role: agentRole,
240
+ agent_goal: agentGoal,
241
+ api_key: k.anthropic || "",
242
+ openai_api_key: k.openai || "",
243
+ google_api_key: k.google || ""
244
+ };
245
+ }
246
+
234
247
  // src/core/models.ts
235
248
  var MODELS = {
236
249
  OPUS: "claude-opus-4-6",
@@ -256,18 +269,11 @@ Starting Nexus Workflow
256
269
  console.log(` Backend: ${backendUrl}
257
270
  `);
258
271
  try {
259
- const keys = loadApiKeys();
272
+ const payload = buildRunPayload(task, options.agent, options.goal || "");
260
273
  const response = await fetch(`${backendUrl}/api/run`, {
261
274
  method: "POST",
262
275
  headers: { "Content-Type": "application/json" },
263
- body: JSON.stringify({
264
- task,
265
- agent_role: options.agent,
266
- agent_goal: options.goal || "",
267
- api_key: keys.anthropic || "",
268
- openai_api_key: keys.openai || "",
269
- google_api_key: keys.google || ""
270
- })
276
+ body: JSON.stringify(payload)
271
277
  });
272
278
  const { run_id } = await response.json();
273
279
  console.log(`Run ID: ${run_id}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buildwithnexus",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Interactive AI agent orchestrator with intent-based planning, execution, and brainstorming modes",
5
5
  "type": "module",
6
6
  "bin": {