@serkanalgur/opencode-nexus 2.5.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 +4454 -242
  3. package/dist/tui.js +176 -48
  4. package/package.json +3 -4
package/dist/tui.js CHANGED
@@ -94,7 +94,7 @@ function describeFile(file) {
94
94
  }
95
95
  function formatConfigLoadLog(info) {
96
96
  const models = Object.entries(info.models).map(([role, model]) => `${role}=${model}`).join(" ");
97
- const override = info.sessionOverride ? " (+session override: storage; disk edits to models are IGNORED while a preset is set \u2014 clear the preset in the TUI to hand control back to disk)" : "";
97
+ const override = info.sessionOverride ? ' (+session override: storage; disk edits to models are IGNORED while a preset is set \u2014 call the preset tool with mode "clear", or reset in the TUI, to hand control back to disk)' : "";
98
98
  return `[nexus] config loaded (#${info.loadCount} trigger=${info.trigger} at=${info.loadedAt}) ` + `project=${info.project.path} [${describeFile(info.project)}] ` + `global=${info.global.path} [${describeFile(info.global)}] ` + `models:${override} ${models}`;
99
99
  }
100
100
  var DEFAULT_CONFIG = {
@@ -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,10 +320,13 @@ 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() {
327
+ const hadOverride = this.storageConfig !== null;
314
328
  this.storageConfig = null;
329
+ return hadOverride;
315
330
  }
316
331
  applyPreset(name) {
317
332
  const preset = PRESETS[name];
@@ -526,6 +541,140 @@ function mergeSidebarAgents(agents, polled, childIDs) {
526
541
  }
527
542
  return next.filter((a) => a.sessionID && childIDs.includes(a.sessionID) || a.status === "completed" || a.status === "failed");
528
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
+ }
529
678
  var tui_default = define({
530
679
  id: "nexus.cli",
531
680
  setup(context) {
@@ -617,10 +766,17 @@ var tui_default = define({
617
766
  variant: "success"
618
767
  });
619
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
+ };
620
776
  const handleDashboard = async () => {
621
777
  const config = configManager.getConfig();
622
778
  const lines = [
623
- "\u26A1 Nexus Dashboard",
779
+ "\u26A1 Nexus Overview",
624
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",
625
781
  "",
626
782
  "\uD83E\uDD16 Agent Models:"
@@ -644,6 +800,7 @@ var tui_default = define({
644
800
  lines.push(" /nexus status - Show config summary");
645
801
  lines.push(" /nexus config - Configure models & budget");
646
802
  lines.push(" /nexus model - Select model for role");
803
+ lines.push(" /nexus web - Open the web dashboard, if one is running");
647
804
  lines.push(" /nexus reset - Reset to defaults");
648
805
  lines.push("");
649
806
  lines.push("\uD83D\uDD27 Tools (use in agent prompt):");
@@ -651,8 +808,14 @@ var tui_default = define({
651
808
  lines.push(" nexus.agents - List spawned agents");
652
809
  lines.push(" nexus.costs - Cost report");
653
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.");
654
817
  context.ui.toast.show({
655
- title: "Nexus Dashboard",
818
+ title: "Nexus Overview",
656
819
  message: lines.join(`
657
820
  `),
658
821
  variant: "info",
@@ -699,30 +862,7 @@ var tui_default = define({
699
862
  break;
700
863
  case "web":
701
864
  case "w":
702
- {
703
- const port = parts[1] ? parseInt(parts[1]) : 4747;
704
- const host = parts[2] || "127.0.0.1";
705
- context.ui.toast.show({
706
- title: "\u26A1 Nexus Web Dashboard",
707
- message: [
708
- `Starting dashboard on port ${port}...`,
709
- "",
710
- `Ask the agent to run: nexus.dashboard.start(port=${port})`,
711
- "",
712
- `Or type: nexus.dashboard.start with port=${port} in your next message`,
713
- "",
714
- `Then open: http://${host}:${port}`
715
- ].join(`
716
- `),
717
- variant: "success",
718
- duration: 8000
719
- });
720
- try {
721
- const { exec } = await import("child_process");
722
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
723
- exec(`${cmd} http://${host}:${port}`);
724
- } catch {}
725
- }
865
+ await runWebDashboard(parts.slice(1).join(" "));
726
866
  break;
727
867
  case "model":
728
868
  case "m":
@@ -783,7 +923,7 @@ var tui_default = define({
783
923
  },
784
924
  {
785
925
  id: "nexus.dashboard",
786
- title: "Nexus Dashboard",
926
+ title: "Nexus Overview (config, budget, dashboard status)",
787
927
  group: "Nexus",
788
928
  palette: true,
789
929
  slash: { name: "nexus-dashboard", aliases: ["nd"], arguments: true },
@@ -795,31 +935,15 @@ var tui_default = define({
795
935
  },
796
936
  {
797
937
  id: "nexus.web",
798
- 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",
799
940
  group: "Nexus",
800
941
  palette: true,
801
942
  slash: { name: "nexus-web", aliases: ["nw"], arguments: true },
802
943
  enabled: () => true,
803
944
  suggested: true,
804
945
  run: async (input) => {
805
- const port = input ? parseInt(input) : 4747;
806
- const host = "127.0.0.1";
807
- context.ui.toast.show({
808
- title: "\u26A1 Nexus Web Dashboard",
809
- message: [
810
- `Port: ${port} Host: ${host}`,
811
- "",
812
- "To start the dashboard, ask the agent:",
813
- ` nexus.dashboard.start(port=${port}, host="${host}")`,
814
- "",
815
- `Then open: http://${host}:${port}`,
816
- "",
817
- "Tip: The dashboard shows live agent status, costs, and history."
818
- ].join(`
819
- `),
820
- variant: "info",
821
- duration: 12000
822
- });
946
+ await runWebDashboard(input);
823
947
  }
824
948
  },
825
949
  {
@@ -1064,7 +1188,11 @@ var tui_default = define({
1064
1188
  export {
1065
1189
  collectSidebarAgents,
1066
1190
  tui_default as default,
1191
+ handleWebDashboard,
1067
1192
  mergeSidebarAgents,
1193
+ openInBrowser,
1194
+ parseWebDashboardTarget,
1195
+ probeDashboard,
1068
1196
  sidebarAgentFor,
1069
1197
  sidebarChildSessionIDs
1070
1198
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serkanalgur/opencode-nexus",
3
- "version": "2.5.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"