@serkanalgur/opencode-nexus 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +113 -40
  2. package/dist/index.js +3950 -174
  3. package/dist/tui.js +173 -47
  4. package/package.json +3 -4
package/dist/tui.js CHANGED
@@ -116,6 +116,11 @@ var DEFAULT_CONFIG = {
116
116
  enabled: true,
117
117
  maxRetries: 3,
118
118
  contextTransfer: true
119
+ },
120
+ dashboard: {
121
+ enabled: true,
122
+ port: 4747,
123
+ host: "127.0.0.1"
119
124
  }
120
125
  };
121
126
 
@@ -124,9 +129,11 @@ class NexusConfigManager {
124
129
  globalConfig = null;
125
130
  storageConfig = null;
126
131
  loadInfo = null;
127
- constructor() {
132
+ dashboardBase;
133
+ constructor(dashboardBase) {
128
134
  this.projectConfig = null;
129
135
  this.globalConfig = null;
136
+ this.dashboardBase = { ...DEFAULT_CONFIG.dashboard, ...dashboardBase };
130
137
  }
131
138
  loadConfigs(basePath) {
132
139
  if (this.loadInfo === null)
@@ -188,6 +195,11 @@ class NexusConfigManager {
188
195
  ...this.globalConfig?.selfHealing,
189
196
  ...this.projectConfig?.selfHealing,
190
197
  ...this.storageConfig?.selfHealing
198
+ },
199
+ dashboard: {
200
+ enabled: this.storageConfig?.dashboard?.enabled ?? this.projectConfig?.dashboard?.enabled ?? this.globalConfig?.dashboard?.enabled ?? this.dashboardBase.enabled,
201
+ port: this.storageConfig?.dashboard?.port ?? this.projectConfig?.dashboard?.port ?? this.globalConfig?.dashboard?.port ?? this.dashboardBase.port,
202
+ host: this.storageConfig?.dashboard?.host ?? this.projectConfig?.dashboard?.host ?? this.globalConfig?.dashboard?.host ?? this.dashboardBase.host
191
203
  }
192
204
  };
193
205
  }
@@ -308,6 +320,7 @@ class NexusConfigManager {
308
320
  result.models = { ...current.models };
309
321
  result.budget = { ...current.budget };
310
322
  result.selfHealing = { ...current.selfHealing };
323
+ result.dashboard = { ...current.dashboard };
311
324
  return result;
312
325
  }
313
326
  resetToDefaults() {
@@ -528,6 +541,140 @@ function mergeSidebarAgents(agents, polled, childIDs) {
528
541
  }
529
542
  return next.filter((a) => a.sessionID && childIDs.includes(a.sessionID) || a.status === "completed" || a.status === "failed");
530
543
  }
544
+ async function probeDashboard(host, port, fetchImpl = fetch) {
545
+ const url = `http://${host}:${port}/api/health`;
546
+ let response;
547
+ try {
548
+ response = await fetchImpl(url);
549
+ } catch (error) {
550
+ const detail = error instanceof Error ? error.message : String(error);
551
+ const refused = /ECONNREFUSED|fetch failed|Failed to fetch|NetworkError|ECONNRESET/i.test(detail);
552
+ return {
553
+ listening: false,
554
+ isNexusDashboard: false,
555
+ detail: refused ? `nothing is listening on ${host}:${port}` : `could not reach ${host}:${port} (${detail})`
556
+ };
557
+ }
558
+ let body;
559
+ try {
560
+ body = await response.json();
561
+ } catch {
562
+ return {
563
+ listening: true,
564
+ isNexusDashboard: false,
565
+ detail: `${host}:${port} answered ${response.status} but not with dashboard JSON`
566
+ };
567
+ }
568
+ if (body !== null && typeof body === "object" && !Array.isArray(body)) {
569
+ const record = body;
570
+ if (record.ok === true && typeof record.uptime === "number") {
571
+ return {
572
+ listening: true,
573
+ isNexusDashboard: true,
574
+ detail: `a nexus dashboard is serving ${host}:${port}`
575
+ };
576
+ }
577
+ }
578
+ return {
579
+ listening: true,
580
+ isNexusDashboard: false,
581
+ detail: `something else is listening on ${host}:${port} and it is not a nexus dashboard`
582
+ };
583
+ }
584
+ function parseWebDashboardTarget(input, fallback) {
585
+ const parts = (input ?? "").trim().split(/\s+/).filter(Boolean);
586
+ let port = fallback.port;
587
+ let host = fallback.host;
588
+ if (parts.length > 0) {
589
+ const parsed = Number(parts[0]);
590
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
591
+ return { error: `"${parts[0]}" is not a port number. Give a port between 1 and 65535.` };
592
+ }
593
+ port = parsed;
594
+ }
595
+ if (parts.length > 1) {
596
+ host = parts[1];
597
+ }
598
+ return { target: { port, host } };
599
+ }
600
+ async function handleWebDashboard(input, deps) {
601
+ const parsed = parseWebDashboardTarget(input, {
602
+ port: deps.dashboard.port,
603
+ host: deps.dashboard.host
604
+ });
605
+ if ("error" in parsed) {
606
+ deps.showToast({ title: "Nexus Web Dashboard", message: parsed.error, variant: "error" });
607
+ return;
608
+ }
609
+ const { port, host } = parsed.target;
610
+ const url = `http://${host}:${port}`;
611
+ if (!deps.dashboard.enabled) {
612
+ deps.showToast({
613
+ title: "Nexus Web Dashboard \u2014 disabled",
614
+ message: [
615
+ `The dashboard is switched off: \`dashboard.enabled\` is false in your nexus.jsonc,`,
616
+ "so nothing will listen on any port and no browser was opened.",
617
+ "",
618
+ "Set it to true (or delete the `dashboard` block, which defaults to enabled),",
619
+ "then run /nexus web again."
620
+ ].join(`
621
+ `),
622
+ variant: "error",
623
+ duration: 12000
624
+ });
625
+ return;
626
+ }
627
+ const probe = await probeDashboard(host, port, deps.fetchImpl);
628
+ if (probe.isNexusDashboard) {
629
+ await deps.openBrowser(url);
630
+ deps.showToast({
631
+ title: "\u26A1 Nexus Web Dashboard",
632
+ message: `Opened ${url} \u2014 a dashboard is already running there.`,
633
+ variant: "success",
634
+ duration: 6000
635
+ });
636
+ return;
637
+ }
638
+ if (probe.listening) {
639
+ deps.showToast({
640
+ title: "Nexus Web Dashboard \u2014 port in use",
641
+ message: [
642
+ `${probe.detail}.`,
643
+ "",
644
+ "No browser was opened: that address is not the dashboard.",
645
+ "Run /nexus web with a different port, e.g. /nexus web 4748."
646
+ ].join(`
647
+ `),
648
+ variant: "error",
649
+ duration: 12000
650
+ });
651
+ return;
652
+ }
653
+ deps.showToast({
654
+ title: "\u26A1 Nexus Web Dashboard",
655
+ message: [
656
+ `No dashboard is running on ${url} (${probe.detail}), and no browser was opened.`,
657
+ "",
658
+ "The dashboard server cannot be started from the TUI: it runs in the OpenCode",
659
+ "server process, next to the orchestrator that feeds it, and the TUI has no",
660
+ "handle on either. So start it from the agent \u2014 this is the one call:",
661
+ "",
662
+ ` nexus.dashboard.start(port=${port}, host="${host}")`,
663
+ "",
664
+ `Then re-run /nexus web and it will open ${url} for you.`
665
+ ].join(`
666
+ `),
667
+ variant: "info",
668
+ duration: 15000
669
+ });
670
+ }
671
+ async function openInBrowser(url) {
672
+ try {
673
+ const { exec } = await import("child_process");
674
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
675
+ exec(`${command} ${url}`);
676
+ } catch {}
677
+ }
531
678
  var tui_default = define({
532
679
  id: "nexus.cli",
533
680
  setup(context) {
@@ -619,10 +766,17 @@ var tui_default = define({
619
766
  variant: "success"
620
767
  });
621
768
  };
769
+ const runWebDashboard = async (input) => {
770
+ await handleWebDashboard(input, {
771
+ dashboard: configManager.getConfig().dashboard,
772
+ showToast: (options) => context.ui.toast.show(options),
773
+ openBrowser: openInBrowser
774
+ });
775
+ };
622
776
  const handleDashboard = async () => {
623
777
  const config = configManager.getConfig();
624
778
  const lines = [
625
- "\u26A1 Nexus Dashboard",
779
+ "\u26A1 Nexus Overview",
626
780
  "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
627
781
  "",
628
782
  "\uD83E\uDD16 Agent Models:"
@@ -646,6 +800,7 @@ var tui_default = define({
646
800
  lines.push(" /nexus status - Show config summary");
647
801
  lines.push(" /nexus config - Configure models & budget");
648
802
  lines.push(" /nexus model - Select model for role");
803
+ lines.push(" /nexus web - Open the web dashboard, if one is running");
649
804
  lines.push(" /nexus reset - Reset to defaults");
650
805
  lines.push("");
651
806
  lines.push("\uD83D\uDD27 Tools (use in agent prompt):");
@@ -653,8 +808,14 @@ var tui_default = define({
653
808
  lines.push(" nexus.agents - List spawned agents");
654
809
  lines.push(" nexus.costs - Cost report");
655
810
  lines.push(" nexus.spawn - Spawn a sub-agent");
811
+ lines.push(" nexus.dashboard.start - Start the web dashboard server");
812
+ lines.push("");
813
+ lines.push("\uD83D\uDCCA Web Dashboard:");
814
+ lines.push(` Enabled: ${config.dashboard.enabled ? "\u2705" : "\u274C"}`);
815
+ lines.push(` Address: http://${config.dashboard.host}:${config.dashboard.port} (when running)`);
816
+ lines.push(" Not running? Ask the agent for nexus.dashboard.start, then /nexus web.");
656
817
  context.ui.toast.show({
657
- title: "Nexus Dashboard",
818
+ title: "Nexus Overview",
658
819
  message: lines.join(`
659
820
  `),
660
821
  variant: "info",
@@ -701,30 +862,7 @@ var tui_default = define({
701
862
  break;
702
863
  case "web":
703
864
  case "w":
704
- {
705
- const port = parts[1] ? parseInt(parts[1]) : 4747;
706
- const host = parts[2] || "127.0.0.1";
707
- context.ui.toast.show({
708
- title: "\u26A1 Nexus Web Dashboard",
709
- message: [
710
- `Starting dashboard on port ${port}...`,
711
- "",
712
- `Ask the agent to run: nexus.dashboard.start(port=${port})`,
713
- "",
714
- `Or type: nexus.dashboard.start with port=${port} in your next message`,
715
- "",
716
- `Then open: http://${host}:${port}`
717
- ].join(`
718
- `),
719
- variant: "success",
720
- duration: 8000
721
- });
722
- try {
723
- const { exec } = await import("child_process");
724
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
725
- exec(`${cmd} http://${host}:${port}`);
726
- } catch {}
727
- }
865
+ await runWebDashboard(parts.slice(1).join(" "));
728
866
  break;
729
867
  case "model":
730
868
  case "m":
@@ -785,7 +923,7 @@ var tui_default = define({
785
923
  },
786
924
  {
787
925
  id: "nexus.dashboard",
788
- title: "Nexus Dashboard",
926
+ title: "Nexus Overview (config, budget, dashboard status)",
789
927
  group: "Nexus",
790
928
  palette: true,
791
929
  slash: { name: "nexus-dashboard", aliases: ["nd"], arguments: true },
@@ -797,31 +935,15 @@ var tui_default = define({
797
935
  },
798
936
  {
799
937
  id: "nexus.web",
800
- title: "Start Nexus Web Dashboard",
938
+ title: "Open the Nexus Web Dashboard",
939
+ description: "Open the dashboard in your browser if one is already serving; otherwise say how to start it",
801
940
  group: "Nexus",
802
941
  palette: true,
803
942
  slash: { name: "nexus-web", aliases: ["nw"], arguments: true },
804
943
  enabled: () => true,
805
944
  suggested: true,
806
945
  run: async (input) => {
807
- const port = input ? parseInt(input) : 4747;
808
- const host = "127.0.0.1";
809
- context.ui.toast.show({
810
- title: "\u26A1 Nexus Web Dashboard",
811
- message: [
812
- `Port: ${port} Host: ${host}`,
813
- "",
814
- "To start the dashboard, ask the agent:",
815
- ` nexus.dashboard.start(port=${port}, host="${host}")`,
816
- "",
817
- `Then open: http://${host}:${port}`,
818
- "",
819
- "Tip: The dashboard shows live agent status, costs, and history."
820
- ].join(`
821
- `),
822
- variant: "info",
823
- duration: 12000
824
- });
946
+ await runWebDashboard(input);
825
947
  }
826
948
  },
827
949
  {
@@ -1066,7 +1188,11 @@ var tui_default = define({
1066
1188
  export {
1067
1189
  collectSidebarAgents,
1068
1190
  tui_default as default,
1191
+ handleWebDashboard,
1069
1192
  mergeSidebarAgents,
1193
+ openInBrowser,
1194
+ parseWebDashboardTarget,
1195
+ probeDashboard,
1070
1196
  sidebarAgentFor,
1071
1197
  sidebarChildSessionIDs
1072
1198
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serkanalgur/opencode-nexus",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Adaptive Multi-Agent Orchestration with Cost Intelligence for OpenCode V2",
5
5
  "keywords": [
6
6
  "opencode",
@@ -44,18 +44,17 @@
44
44
  "dev": "bun run --watch src/index.ts",
45
45
  "typecheck": "tsc --noEmit",
46
46
  "test": "bun test",
47
- "lint": "biome check src/",
48
- "format": "biome format --write src/",
47
+ "lint": "biome check src/ test/ && tsc --noEmit && bun -e 'const page = \"dashboard/index.html\"; const html = await Bun.file(page).text(); const inline = [...html.matchAll(/<script(?![^>]*\\bsrc=)[^>]*>([\\s\\S]*?)<\\/script>/g)]; if (inline.length === 0) throw new Error(page + \" has no inline <script> to check\"); inline.forEach(m => { new Function(m[1]); }); console.log(\"lint: \" + page + \" - \" + inline.length + \" inline script(s) parse clean\");'",
49
48
  "prepublishOnly": "bun run build"
50
49
  },
51
50
  "dependencies": {
52
51
  "@opencode/plugin": "latest"
53
52
  },
54
53
  "devDependencies": {
54
+ "@biomejs/biome": "latest",
55
55
  "@opentui/core": "^0.5.8",
56
56
  "@opentui/solid": "^0.5.8",
57
57
  "@types/bun": "latest",
58
- "biome": "latest",
59
58
  "bun-types": "^1.4.2",
60
59
  "solid-js": "^1.9.0",
61
60
  "typescript": "^5.5.0"