@serkanalgur/opencode-nexus 2.6.0 → 2.8.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 +148 -40
  2. package/dist/index.js +4628 -218
  3. package/dist/tui.js +266 -52
  4. package/package.json +3 -4
package/dist/tui.js CHANGED
@@ -116,6 +116,14 @@ 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"
124
+ },
125
+ notifications: {
126
+ enabled: true
119
127
  }
120
128
  };
121
129
 
@@ -124,9 +132,13 @@ class NexusConfigManager {
124
132
  globalConfig = null;
125
133
  storageConfig = null;
126
134
  loadInfo = null;
127
- constructor() {
135
+ dashboardBase;
136
+ notificationsBase;
137
+ constructor(dashboardBase, notificationsBase) {
128
138
  this.projectConfig = null;
129
139
  this.globalConfig = null;
140
+ this.dashboardBase = { ...DEFAULT_CONFIG.dashboard, ...dashboardBase };
141
+ this.notificationsBase = { ...DEFAULT_CONFIG.notifications, ...notificationsBase };
130
142
  }
131
143
  loadConfigs(basePath) {
132
144
  if (this.loadInfo === null)
@@ -188,6 +200,14 @@ class NexusConfigManager {
188
200
  ...this.globalConfig?.selfHealing,
189
201
  ...this.projectConfig?.selfHealing,
190
202
  ...this.storageConfig?.selfHealing
203
+ },
204
+ dashboard: {
205
+ enabled: this.storageConfig?.dashboard?.enabled ?? this.projectConfig?.dashboard?.enabled ?? this.globalConfig?.dashboard?.enabled ?? this.dashboardBase.enabled,
206
+ port: this.storageConfig?.dashboard?.port ?? this.projectConfig?.dashboard?.port ?? this.globalConfig?.dashboard?.port ?? this.dashboardBase.port,
207
+ host: this.storageConfig?.dashboard?.host ?? this.projectConfig?.dashboard?.host ?? this.globalConfig?.dashboard?.host ?? this.dashboardBase.host
208
+ },
209
+ notifications: {
210
+ enabled: this.storageConfig?.notifications?.enabled ?? this.projectConfig?.notifications?.enabled ?? this.globalConfig?.notifications?.enabled ?? this.notificationsBase.enabled
191
211
  }
192
212
  };
193
213
  }
@@ -308,6 +328,8 @@ class NexusConfigManager {
308
328
  result.models = { ...current.models };
309
329
  result.budget = { ...current.budget };
310
330
  result.selfHealing = { ...current.selfHealing };
331
+ result.dashboard = { ...current.dashboard };
332
+ result.notifications = { ...current.notifications };
311
333
  return result;
312
334
  }
313
335
  resetToDefaults() {
@@ -528,6 +550,185 @@ function mergeSidebarAgents(agents, polled, childIDs) {
528
550
  }
529
551
  return next.filter((a) => a.sessionID && childIDs.includes(a.sessionID) || a.status === "completed" || a.status === "failed");
530
552
  }
553
+ async function probeDashboard(host, port, fetchImpl = fetch) {
554
+ const url = `http://${host}:${port}/api/health`;
555
+ let response;
556
+ try {
557
+ response = await fetchImpl(url);
558
+ } catch (error) {
559
+ const detail = error instanceof Error ? error.message : String(error);
560
+ const refused = /ECONNREFUSED|fetch failed|Failed to fetch|NetworkError|ECONNRESET/i.test(detail);
561
+ return {
562
+ listening: false,
563
+ isNexusDashboard: false,
564
+ detail: refused ? `nothing is listening on ${host}:${port}` : `could not reach ${host}:${port} (${detail})`
565
+ };
566
+ }
567
+ let body;
568
+ try {
569
+ body = await response.json();
570
+ } catch {
571
+ return {
572
+ listening: true,
573
+ isNexusDashboard: false,
574
+ detail: `${host}:${port} answered ${response.status} but not with dashboard JSON`
575
+ };
576
+ }
577
+ if (body !== null && typeof body === "object" && !Array.isArray(body)) {
578
+ const record = body;
579
+ if (record.ok === true && typeof record.uptime === "number") {
580
+ return {
581
+ listening: true,
582
+ isNexusDashboard: true,
583
+ detail: `a nexus dashboard is serving ${host}:${port}`
584
+ };
585
+ }
586
+ }
587
+ return {
588
+ listening: true,
589
+ isNexusDashboard: false,
590
+ detail: `something else is listening on ${host}:${port} and it is not a nexus dashboard`
591
+ };
592
+ }
593
+ function parseWebDashboardTarget(input, fallback) {
594
+ const parts = (input ?? "").trim().split(/\s+/).filter(Boolean);
595
+ let port = fallback.port;
596
+ let host = fallback.host;
597
+ if (parts.length > 0) {
598
+ const parsed = Number(parts[0]);
599
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
600
+ return { error: `"${parts[0]}" is not a port number. Give a port between 1 and 65535.` };
601
+ }
602
+ port = parsed;
603
+ }
604
+ if (parts.length > 1) {
605
+ host = parts[1];
606
+ }
607
+ return { target: { port, host } };
608
+ }
609
+ var LISTEN_CONFIRM_ATTEMPTS = 15;
610
+ var LISTEN_CONFIRM_INTERVAL_MS = 200;
611
+ function defaultWait(ms) {
612
+ return new Promise((resolve) => setTimeout(resolve, ms));
613
+ }
614
+ async function handleWebDashboard(input, deps) {
615
+ const parsed = parseWebDashboardTarget(input, {
616
+ port: deps.dashboard.port,
617
+ host: deps.dashboard.host
618
+ });
619
+ if ("error" in parsed) {
620
+ deps.showToast({ title: "Nexus Web Dashboard", message: parsed.error, variant: "error" });
621
+ return;
622
+ }
623
+ const { port, host } = parsed.target;
624
+ const url = `http://${host}:${port}`;
625
+ if (!deps.dashboard.enabled) {
626
+ deps.showToast({
627
+ title: "Nexus Web Dashboard \u2014 disabled",
628
+ message: [
629
+ `The dashboard is switched off: \`dashboard.enabled\` is false in your nexus.jsonc,`,
630
+ "so nothing will listen on any port and no browser was opened.",
631
+ "",
632
+ "Set it to true (or delete the `dashboard` block, which defaults to enabled),",
633
+ "then run /nexus web again."
634
+ ].join(`
635
+ `),
636
+ variant: "error",
637
+ duration: 12000
638
+ });
639
+ return;
640
+ }
641
+ const probe = await probeDashboard(host, port, deps.fetchImpl);
642
+ if (probe.isNexusDashboard) {
643
+ await deps.openBrowser(url);
644
+ deps.showToast({
645
+ title: "\u26A1 Nexus Web Dashboard",
646
+ message: `Opened ${url} \u2014 a dashboard is already running there, so nothing was started.`,
647
+ variant: "success",
648
+ duration: 6000
649
+ });
650
+ return;
651
+ }
652
+ if (probe.listening) {
653
+ deps.showToast({
654
+ title: "Nexus Web Dashboard \u2014 port in use",
655
+ message: [
656
+ `${probe.detail}.`,
657
+ "",
658
+ "No browser was opened: that address is not the dashboard.",
659
+ "Run /nexus web with a different port, e.g. /nexus web 4748."
660
+ ].join(`
661
+ `),
662
+ variant: "error",
663
+ duration: 12000
664
+ });
665
+ return;
666
+ }
667
+ let submitted;
668
+ try {
669
+ await deps.submitCommand(`/nexus dashboard ${port} ${host}`);
670
+ submitted = `/nexus dashboard ${port} ${host}`;
671
+ } catch (error) {
672
+ const detail = error instanceof Error ? error.message : String(error);
673
+ deps.showToast({
674
+ title: "Nexus Web Dashboard \u2014 could not start",
675
+ message: [
676
+ `The dashboard start command never reached the OpenCode server: ${detail}`,
677
+ "",
678
+ "No server is listening on that port and no browser was opened.",
679
+ "The dashboard runs in the server process, so this needs the server's",
680
+ "HTTP endpoint to be reachable from the TUI."
681
+ ].join(`
682
+ `),
683
+ variant: "error",
684
+ duration: 15000
685
+ });
686
+ return;
687
+ }
688
+ const wait = deps.waitImpl ?? defaultWait;
689
+ const attempts = deps.confirmAttempts ?? LISTEN_CONFIRM_ATTEMPTS;
690
+ const intervalMs = deps.confirmIntervalMs ?? LISTEN_CONFIRM_INTERVAL_MS;
691
+ for (let attempt = 0;attempt < attempts; attempt++) {
692
+ if (attempt > 0)
693
+ await wait(intervalMs);
694
+ const check = await probeDashboard(host, port, deps.fetchImpl);
695
+ if (check.isNexusDashboard) {
696
+ await deps.openBrowser(url);
697
+ deps.showToast({
698
+ title: "\u26A1 Nexus Web Dashboard",
699
+ message: `Started and opened ${url} \u2014 a dashboard is serving there now.`,
700
+ variant: "success",
701
+ duration: 8000
702
+ });
703
+ return;
704
+ }
705
+ }
706
+ deps.showToast({
707
+ title: "Nexus Web Dashboard \u2014 not started",
708
+ message: [
709
+ `Ran \`${submitted}\`, but nothing is serving on ${host}:${port} after ${Math.round(attempts * intervalMs / 1000)}s,`,
710
+ "so no browser was opened and there is no URL to give you.",
711
+ "",
712
+ "The command runs in the OpenCode server process, which is the only place",
713
+ "the dashboard can start. If a Nexus plugin is active in that process, its",
714
+ `result is in this session as the reply to \`${submitted}\` \u2014 that text names`,
715
+ "the reason (a port already in use, or `dashboard.enabled: false`). If no",
716
+ "plugin is active there, the command was never handled at all. If the port",
717
+ "is held by another process, free it or pass a different one:",
718
+ "/nexus web 4748."
719
+ ].join(`
720
+ `),
721
+ variant: "error",
722
+ duration: 20000
723
+ });
724
+ }
725
+ async function openInBrowser(url) {
726
+ try {
727
+ const { exec } = await import("child_process");
728
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
729
+ exec(`${command} ${url}`);
730
+ } catch {}
731
+ }
531
732
  var tui_default = define({
532
733
  id: "nexus.cli",
533
734
  setup(context) {
@@ -619,10 +820,31 @@ var tui_default = define({
619
820
  variant: "success"
620
821
  });
621
822
  };
622
- const handleDashboard = async () => {
823
+ const activeSessionID = () => {
824
+ const route = context.ui.router.current();
825
+ if (route.type === "session")
826
+ return route.sessionID;
827
+ return context.ui.tabs.list().find((tab) => tab.active)?.sessionID;
828
+ };
829
+ const submitServerCommand = async (text) => {
830
+ const sessionID = activeSessionID();
831
+ if (!sessionID) {
832
+ throw new Error("no session is open to run the command in");
833
+ }
834
+ await context.client.session.prompt({ sessionID, text });
835
+ };
836
+ const runWebDashboard = async (input) => {
837
+ await handleWebDashboard(input, {
838
+ dashboard: configManager.getConfig().dashboard,
839
+ showToast: (options) => context.ui.toast.show(options),
840
+ openBrowser: openInBrowser,
841
+ submitCommand: submitServerCommand
842
+ });
843
+ };
844
+ const handleOverview = async () => {
623
845
  const config = configManager.getConfig();
624
846
  const lines = [
625
- "\u26A1 Nexus Dashboard",
847
+ "\u26A1 Nexus Overview",
626
848
  "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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
849
  "",
628
850
  "\uD83E\uDD16 Agent Models:"
@@ -646,6 +868,9 @@ var tui_default = define({
646
868
  lines.push(" /nexus status - Show config summary");
647
869
  lines.push(" /nexus config - Configure models & budget");
648
870
  lines.push(" /nexus model - Select model for role");
871
+ lines.push(" /nexus dashboard - Start the web dashboard and open it");
872
+ lines.push(" /nexus web - Same: start it if needed, then open it");
873
+ lines.push(" /nexus overview - Show this overview");
649
874
  lines.push(" /nexus reset - Reset to defaults");
650
875
  lines.push("");
651
876
  lines.push("\uD83D\uDD27 Tools (use in agent prompt):");
@@ -653,8 +878,14 @@ var tui_default = define({
653
878
  lines.push(" nexus.agents - List spawned agents");
654
879
  lines.push(" nexus.costs - Cost report");
655
880
  lines.push(" nexus.spawn - Spawn a sub-agent");
881
+ lines.push(" nexus.dashboard.start - Start the web dashboard server");
882
+ lines.push("");
883
+ lines.push("\uD83D\uDCCA Web Dashboard:");
884
+ lines.push(` Enabled: ${config.dashboard.enabled ? "\u2705" : "\u274C"}`);
885
+ lines.push(` Address: http://${config.dashboard.host}:${config.dashboard.port} (when running)`);
886
+ lines.push(" Not running? /nexus dashboard starts it and opens it.");
656
887
  context.ui.toast.show({
657
- title: "Nexus Dashboard",
888
+ title: "Nexus Overview",
658
889
  message: lines.join(`
659
890
  `),
660
891
  variant: "info",
@@ -697,34 +928,14 @@ var tui_default = define({
697
928
  break;
698
929
  case "dashboard":
699
930
  case "d":
700
- await handleDashboard();
931
+ await runWebDashboard(parts.slice(1).join(" "));
932
+ break;
933
+ case "overview":
934
+ await handleOverview();
701
935
  break;
702
936
  case "web":
703
937
  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
- }
938
+ await runWebDashboard(parts.slice(1).join(" "));
728
939
  break;
729
940
  case "model":
730
941
  case "m":
@@ -762,7 +973,7 @@ var tui_default = define({
762
973
  default:
763
974
  context.ui.toast.show({
764
975
  title: "Nexus",
765
- message: "Commands: config, status, dashboard, web [port], model <role>, reset",
976
+ message: "Commands: config, status, dashboard [port], web [port], overview, model <role>, reset",
766
977
  variant: "info"
767
978
  });
768
979
  }
@@ -783,45 +994,42 @@ var tui_default = define({
783
994
  await handleFullConfig();
784
995
  }
785
996
  },
997
+ {
998
+ id: "nexus.overview",
999
+ title: "Nexus Overview (config, budget, dashboard status)",
1000
+ group: "Nexus",
1001
+ palette: true,
1002
+ slash: { name: "nexus-overview", aliases: ["no"], arguments: true },
1003
+ enabled: () => true,
1004
+ suggested: true,
1005
+ run: async () => {
1006
+ await handleOverview();
1007
+ }
1008
+ },
786
1009
  {
787
1010
  id: "nexus.dashboard",
788
- title: "Nexus Dashboard",
1011
+ title: "Start the Nexus Web Dashboard",
1012
+ description: "Starts the dashboard server if it is not already running, then opens it in your browser",
789
1013
  group: "Nexus",
790
1014
  palette: true,
791
1015
  slash: { name: "nexus-dashboard", aliases: ["nd"], arguments: true },
792
1016
  enabled: () => true,
793
1017
  suggested: true,
794
- run: async () => {
795
- await handleDashboard();
1018
+ run: async (input) => {
1019
+ await runWebDashboard(input);
796
1020
  }
797
1021
  },
798
1022
  {
799
1023
  id: "nexus.web",
800
- title: "Start Nexus Web Dashboard",
1024
+ title: "Open the Nexus Web Dashboard",
1025
+ description: "Alias of Nexus Dashboard: starts it if needed, then opens it in your browser",
801
1026
  group: "Nexus",
802
1027
  palette: true,
803
1028
  slash: { name: "nexus-web", aliases: ["nw"], arguments: true },
804
1029
  enabled: () => true,
805
1030
  suggested: true,
806
1031
  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
- });
1032
+ await runWebDashboard(input);
825
1033
  }
826
1034
  },
827
1035
  {
@@ -1064,9 +1272,15 @@ var tui_default = define({
1064
1272
  }
1065
1273
  });
1066
1274
  export {
1275
+ LISTEN_CONFIRM_ATTEMPTS,
1276
+ LISTEN_CONFIRM_INTERVAL_MS,
1067
1277
  collectSidebarAgents,
1068
1278
  tui_default as default,
1279
+ handleWebDashboard,
1069
1280
  mergeSidebarAgents,
1281
+ openInBrowser,
1282
+ parseWebDashboardTarget,
1283
+ probeDashboard,
1070
1284
  sidebarAgentFor,
1071
1285
  sidebarChildSessionIDs
1072
1286
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serkanalgur/opencode-nexus",
3
- "version": "2.6.0",
3
+ "version": "2.8.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"