zenox 1.0.0 → 1.0.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/README.md CHANGED
@@ -9,7 +9,7 @@ OpenCode plugin for intelligent agent orchestration with specialized subagents a
9
9
  - **Auto-Update**: Automatic update checking with toast notifications
10
10
  - **Auto-Loaded MCP Servers**: exa, grep_app, sequential-thinking
11
11
  - **Smart Orchestration**: Automatically teaches Build/Plan agents how to delegate
12
- - **CLI Installer**: Easy setup via `bunx zenox install`
12
+ - **CLI Installer**: Easy setup with model configuration via `bunx zenox install`
13
13
 
14
14
  ## Installation
15
15
 
@@ -22,7 +22,8 @@ bunx zenox install
22
22
  This will:
23
23
  - Create `opencode.json` if it doesn't exist
24
24
  - Add zenox to your plugins array
25
- - Preserve existing configuration
25
+ - Let you configure custom models for each agent (optional)
26
+ - Detect models from your existing opencode.json config
26
27
 
27
28
  ### Manual Install
28
29
 
@@ -34,19 +35,23 @@ Add to your `opencode.json`:
34
35
  }
35
36
  ```
36
37
 
37
- > **Note**: Don't pin the version (e.g., `zenox@0.1.0`) if you want automatic updates.
38
+ > **Note**: Don't pin the version (e.g., `zenox@1.0.0`) if you want automatic updates.
38
39
 
39
40
  Restart OpenCode and the plugin is ready to use.
40
41
 
41
42
  ## CLI Commands
42
43
 
43
44
  ```bash
44
- # Interactive install with TUI
45
+ # Interactive install with model configuration
45
46
  bunx zenox install
46
47
 
47
- # Non-interactive mode (CI/scripts)
48
+ # Non-interactive mode (uses default models)
48
49
  bunx zenox install --no-tui
49
50
 
51
+ # Reconfigure agent models later
52
+ bunx zenox config
53
+ bunx zenox models # alias
54
+
50
55
  # Specify config path
51
56
  bunx zenox install --config ./path/to/opencode.json
52
57
 
@@ -54,6 +59,18 @@ bunx zenox install --config ./path/to/opencode.json
54
59
  bunx zenox --help
55
60
  ```
56
61
 
62
+ ### Model Configuration
63
+
64
+ During installation, you can choose to customize models for each agent:
65
+
66
+ 1. **Use recommended defaults** - No config file needed
67
+ 2. **Customize models** - Pick from:
68
+ - Recommended default model
69
+ - Models defined in your opencode.json
70
+ - Enter a custom model name (see [models.dev](https://models.dev))
71
+
72
+ Custom models are saved to `~/.config/opencode/zenox.json`.
73
+
57
74
  ## Agents
58
75
 
59
76
  ### @explorer
@@ -62,7 +79,7 @@ Fast codebase search specialist.
62
79
  - "Find all files containing Y"
63
80
  - Pattern matching and locating implementations
64
81
 
65
- **Model**: `anthropic/claude-haiku-4-5`
82
+ **Default Model**: `anthropic/claude-haiku-4-5`
66
83
 
67
84
  ### @librarian
68
85
  Open-source research agent.
@@ -70,7 +87,7 @@ Open-source research agent.
70
87
  - "Show me implementation examples"
71
88
  - Finding official documentation
72
89
 
73
- **Model**: `anthropic/claude-sonnet-4-5`
90
+ **Default Model**: `anthropic/claude-sonnet-4-5`
74
91
 
75
92
  ### @oracle
76
93
  Strategic technical advisor.
@@ -78,7 +95,7 @@ Strategic technical advisor.
78
95
  - Code review and debugging strategy
79
96
  - Technical trade-offs analysis
80
97
 
81
- **Model**: `openai/gpt-5.2-high`
98
+ **Default Model**: `openai/gpt-5.2`
82
99
 
83
100
  ### @ui-planner
84
101
  Designer-turned-developer.
@@ -86,7 +103,7 @@ Designer-turned-developer.
86
103
  - Frontend aesthetics and animations
87
104
  - Visual design without mockups
88
105
 
89
- **Model**: `google/gemini-3-pro-high`
106
+ **Default Model**: `google/gemini-3-pro-high`
90
107
 
91
108
  ## Background Tasks
92
109
 
@@ -122,11 +139,21 @@ Zenox automatically checks for updates on startup:
122
139
  3. **Cache Invalidation**: If update available, clears Bun cache
123
140
  4. **Update Toast**: Notifies you to restart OpenCode
124
141
 
125
- To disable auto-updates, pin your version: `"zenox@0.1.0"`
142
+ To disable auto-updates, pin your version: `"zenox@1.0.0"`
143
+
144
+ ## Configuration
145
+
146
+ ### Via CLI (Recommended)
147
+
148
+ ```bash
149
+ bunx zenox config
150
+ ```
151
+
152
+ Interactive UI to select models for each agent.
126
153
 
127
- ## Configuration (Optional)
154
+ ### Manual Configuration
128
155
 
129
- Create `~/.config/opencode/zenox.json` or `.opencode/zenox.json`:
156
+ Create `~/.config/opencode/zenox.json`:
130
157
 
131
158
  ```json
132
159
  {
@@ -0,0 +1 @@
1
+ export declare function runConfig(): Promise<void>;
@@ -0,0 +1,10 @@
1
+ export declare const PACKAGE_NAME = "zenox";
2
+ export type AgentName = "explorer" | "librarian" | "oracle" | "ui-planner";
3
+ export interface AgentInfo {
4
+ name: AgentName;
5
+ displayName: string;
6
+ defaultModel: string;
7
+ description: string;
8
+ }
9
+ export declare const AGENTS: AgentInfo[];
10
+ export declare const DEFAULT_MODELS: Record<AgentName, string>;
package/dist/cli/index.js CHANGED
@@ -2258,6 +2258,7 @@ var {
2258
2258
 
2259
2259
  // node_modules/@clack/core/dist/index.mjs
2260
2260
  var import_sisteransi = __toESM(require_src(), 1);
2261
+ var import_picocolors = __toESM(require_picocolors(), 1);
2261
2262
  import { stdin as j, stdout as M } from "node:process";
2262
2263
  import * as g from "node:readline";
2263
2264
  import O from "node:readline";
@@ -2632,9 +2633,89 @@ class dD extends x {
2632
2633
  }
2633
2634
  var A;
2634
2635
  A = new WeakMap;
2636
+ var kD = Object.defineProperty;
2637
+ var $D = (e, u, t) => (u in e) ? kD(e, u, { enumerable: true, configurable: true, writable: true, value: t }) : e[u] = t;
2638
+ var H = (e, u, t) => ($D(e, typeof u != "symbol" ? u + "" : u, t), t);
2639
+ var SD = class extends x {
2640
+ constructor(u) {
2641
+ super(u, false), H(this, "options"), H(this, "cursor", 0), this.options = u.options, this.value = [...u.initialValues ?? []], this.cursor = Math.max(this.options.findIndex(({ value: t }) => t === u.cursorAt), 0), this.on("key", (t) => {
2642
+ t === "a" && this.toggleAll();
2643
+ }), this.on("cursor", (t) => {
2644
+ switch (t) {
2645
+ case "left":
2646
+ case "up":
2647
+ this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
2648
+ break;
2649
+ case "down":
2650
+ case "right":
2651
+ this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
2652
+ break;
2653
+ case "space":
2654
+ this.toggleValue();
2655
+ break;
2656
+ }
2657
+ });
2658
+ }
2659
+ get _value() {
2660
+ return this.options[this.cursor].value;
2661
+ }
2662
+ toggleAll() {
2663
+ const u = this.value.length === this.options.length;
2664
+ this.value = u ? [] : this.options.map((t) => t.value);
2665
+ }
2666
+ toggleValue() {
2667
+ const u = this.value.includes(this._value);
2668
+ this.value = u ? this.value.filter((t) => t !== this._value) : [...this.value, this._value];
2669
+ }
2670
+ };
2671
+ var OD = Object.defineProperty;
2672
+ var PD = (e, u, t) => (u in e) ? OD(e, u, { enumerable: true, configurable: true, writable: true, value: t }) : e[u] = t;
2673
+ var J = (e, u, t) => (PD(e, typeof u != "symbol" ? u + "" : u, t), t);
2674
+
2675
+ class LD extends x {
2676
+ constructor(u) {
2677
+ super(u, false), J(this, "options"), J(this, "cursor", 0), this.options = u.options, this.cursor = this.options.findIndex(({ value: t }) => t === u.initialValue), this.cursor === -1 && (this.cursor = 0), this.changeValue(), this.on("cursor", (t) => {
2678
+ switch (t) {
2679
+ case "left":
2680
+ case "up":
2681
+ this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
2682
+ break;
2683
+ case "down":
2684
+ case "right":
2685
+ this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
2686
+ break;
2687
+ }
2688
+ this.changeValue();
2689
+ });
2690
+ }
2691
+ get _value() {
2692
+ return this.options[this.cursor];
2693
+ }
2694
+ changeValue() {
2695
+ this.value = this._value.value;
2696
+ }
2697
+ }
2698
+ class RD extends x {
2699
+ get valueWithCursor() {
2700
+ if (this.state === "submit")
2701
+ return this.value;
2702
+ if (this.cursor >= this.value.length)
2703
+ return `${this.value}█`;
2704
+ const u = this.value.slice(0, this.cursor), [t, ...F] = this.value.slice(this.cursor);
2705
+ return `${u}${import_picocolors.default.inverse(t)}${F.join("")}`;
2706
+ }
2707
+ get cursor() {
2708
+ return this._cursor;
2709
+ }
2710
+ constructor(u) {
2711
+ super(u), this.on("finalize", () => {
2712
+ this.value || (this.value = u.defaultValue);
2713
+ });
2714
+ }
2715
+ }
2635
2716
 
2636
2717
  // node_modules/@clack/prompts/dist/index.mjs
2637
- var import_picocolors = __toESM(require_picocolors(), 1);
2718
+ var import_picocolors2 = __toESM(require_picocolors(), 1);
2638
2719
  var import_sisteransi2 = __toESM(require_src(), 1);
2639
2720
  import y2 from "node:process";
2640
2721
  function ce() {
@@ -2667,83 +2748,187 @@ var b2 = (t) => {
2667
2748
  switch (t) {
2668
2749
  case "initial":
2669
2750
  case "active":
2670
- return import_picocolors.default.cyan(le);
2751
+ return import_picocolors2.default.cyan(le);
2671
2752
  case "cancel":
2672
- return import_picocolors.default.red(L2);
2753
+ return import_picocolors2.default.red(L2);
2673
2754
  case "error":
2674
- return import_picocolors.default.yellow(W2);
2755
+ return import_picocolors2.default.yellow(W2);
2675
2756
  case "submit":
2676
- return import_picocolors.default.green(C);
2757
+ return import_picocolors2.default.green(C);
2677
2758
  }
2678
2759
  };
2760
+ var G2 = (t) => {
2761
+ const { cursor: n, options: r2, style: i } = t, s = t.maxItems ?? Number.POSITIVE_INFINITY, c = Math.max(process.stdout.rows - 4, 0), a = Math.min(c, Math.max(s, 5));
2762
+ let l2 = 0;
2763
+ n >= l2 + a - 3 ? l2 = Math.max(Math.min(n - a + 3, r2.length - a), 0) : n < l2 + 2 && (l2 = Math.max(n - 2, 0));
2764
+ const $2 = a < r2.length && l2 > 0, g2 = a < r2.length && l2 + a < r2.length;
2765
+ return r2.slice(l2, l2 + a).map((p2, v2, f) => {
2766
+ const j2 = v2 === 0 && $2, E = v2 === f.length - 1 && g2;
2767
+ return j2 || E ? import_picocolors2.default.dim("...") : i(p2, v2 + l2 === n);
2768
+ });
2769
+ };
2770
+ var he = (t) => new RD({ validate: t.validate, placeholder: t.placeholder, defaultValue: t.defaultValue, initialValue: t.initialValue, render() {
2771
+ const n = `${import_picocolors2.default.gray(o)}
2772
+ ${b2(this.state)} ${t.message}
2773
+ `, r2 = t.placeholder ? import_picocolors2.default.inverse(t.placeholder[0]) + import_picocolors2.default.dim(t.placeholder.slice(1)) : import_picocolors2.default.inverse(import_picocolors2.default.hidden("_")), i = this.value ? this.valueWithCursor : r2;
2774
+ switch (this.state) {
2775
+ case "error":
2776
+ return `${n.trim()}
2777
+ ${import_picocolors2.default.yellow(o)} ${i}
2778
+ ${import_picocolors2.default.yellow(d2)} ${import_picocolors2.default.yellow(this.error)}
2779
+ `;
2780
+ case "submit":
2781
+ return `${n}${import_picocolors2.default.gray(o)} ${import_picocolors2.default.dim(this.value || t.placeholder)}`;
2782
+ case "cancel":
2783
+ return `${n}${import_picocolors2.default.gray(o)} ${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(this.value ?? ""))}${this.value?.trim() ? `
2784
+ ${import_picocolors2.default.gray(o)}` : ""}`;
2785
+ default:
2786
+ return `${n}${import_picocolors2.default.cyan(o)} ${i}
2787
+ ${import_picocolors2.default.cyan(d2)}
2788
+ `;
2789
+ }
2790
+ } }).prompt();
2679
2791
  var ye = (t) => {
2680
2792
  const n = t.active ?? "Yes", r2 = t.inactive ?? "No";
2681
2793
  return new dD({ active: n, inactive: r2, initialValue: t.initialValue ?? true, render() {
2682
- const i = `${import_picocolors.default.gray(o)}
2794
+ const i = `${import_picocolors2.default.gray(o)}
2683
2795
  ${b2(this.state)} ${t.message}
2684
2796
  `, s = this.value ? n : r2;
2685
2797
  switch (this.state) {
2686
2798
  case "submit":
2687
- return `${i}${import_picocolors.default.gray(o)} ${import_picocolors.default.dim(s)}`;
2799
+ return `${i}${import_picocolors2.default.gray(o)} ${import_picocolors2.default.dim(s)}`;
2800
+ case "cancel":
2801
+ return `${i}${import_picocolors2.default.gray(o)} ${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(s))}
2802
+ ${import_picocolors2.default.gray(o)}`;
2803
+ default:
2804
+ return `${i}${import_picocolors2.default.cyan(o)} ${this.value ? `${import_picocolors2.default.green(k2)} ${n}` : `${import_picocolors2.default.dim(P2)} ${import_picocolors2.default.dim(n)}`} ${import_picocolors2.default.dim("/")} ${this.value ? `${import_picocolors2.default.dim(P2)} ${import_picocolors2.default.dim(r2)}` : `${import_picocolors2.default.green(k2)} ${r2}`}
2805
+ ${import_picocolors2.default.cyan(d2)}
2806
+ `;
2807
+ }
2808
+ } }).prompt();
2809
+ };
2810
+ var ve = (t) => {
2811
+ const n = (r2, i) => {
2812
+ const s = r2.label ?? String(r2.value);
2813
+ switch (i) {
2814
+ case "selected":
2815
+ return `${import_picocolors2.default.dim(s)}`;
2816
+ case "active":
2817
+ return `${import_picocolors2.default.green(k2)} ${s} ${r2.hint ? import_picocolors2.default.dim(`(${r2.hint})`) : ""}`;
2818
+ case "cancelled":
2819
+ return `${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(s))}`;
2820
+ default:
2821
+ return `${import_picocolors2.default.dim(P2)} ${import_picocolors2.default.dim(s)}`;
2822
+ }
2823
+ };
2824
+ return new LD({ options: t.options, initialValue: t.initialValue, render() {
2825
+ const r2 = `${import_picocolors2.default.gray(o)}
2826
+ ${b2(this.state)} ${t.message}
2827
+ `;
2828
+ switch (this.state) {
2829
+ case "submit":
2830
+ return `${r2}${import_picocolors2.default.gray(o)} ${n(this.options[this.cursor], "selected")}`;
2688
2831
  case "cancel":
2689
- return `${i}${import_picocolors.default.gray(o)} ${import_picocolors.default.strikethrough(import_picocolors.default.dim(s))}
2690
- ${import_picocolors.default.gray(o)}`;
2832
+ return `${r2}${import_picocolors2.default.gray(o)} ${n(this.options[this.cursor], "cancelled")}
2833
+ ${import_picocolors2.default.gray(o)}`;
2691
2834
  default:
2692
- return `${i}${import_picocolors.default.cyan(o)} ${this.value ? `${import_picocolors.default.green(k2)} ${n}` : `${import_picocolors.default.dim(P2)} ${import_picocolors.default.dim(n)}`} ${import_picocolors.default.dim("/")} ${this.value ? `${import_picocolors.default.dim(P2)} ${import_picocolors.default.dim(r2)}` : `${import_picocolors.default.green(k2)} ${r2}`}
2693
- ${import_picocolors.default.cyan(d2)}
2835
+ return `${r2}${import_picocolors2.default.cyan(o)} ${G2({ cursor: this.cursor, options: this.options, maxItems: t.maxItems, style: (i, s) => n(i, s ? "active" : "inactive") }).join(`
2836
+ ${import_picocolors2.default.cyan(o)} `)}
2837
+ ${import_picocolors2.default.cyan(d2)}
2838
+ `;
2839
+ }
2840
+ } }).prompt();
2841
+ };
2842
+ var fe = (t) => {
2843
+ const n = (r2, i) => {
2844
+ const s = r2.label ?? String(r2.value);
2845
+ return i === "active" ? `${import_picocolors2.default.cyan(A2)} ${s} ${r2.hint ? import_picocolors2.default.dim(`(${r2.hint})`) : ""}` : i === "selected" ? `${import_picocolors2.default.green(T)} ${import_picocolors2.default.dim(s)} ${r2.hint ? import_picocolors2.default.dim(`(${r2.hint})`) : ""}` : i === "cancelled" ? `${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(s))}` : i === "active-selected" ? `${import_picocolors2.default.green(T)} ${s} ${r2.hint ? import_picocolors2.default.dim(`(${r2.hint})`) : ""}` : i === "submitted" ? `${import_picocolors2.default.dim(s)}` : `${import_picocolors2.default.dim(F)} ${import_picocolors2.default.dim(s)}`;
2846
+ };
2847
+ return new SD({ options: t.options, initialValues: t.initialValues, required: t.required ?? true, cursorAt: t.cursorAt, validate(r2) {
2848
+ if (this.required && r2.length === 0)
2849
+ return `Please select at least one option.
2850
+ ${import_picocolors2.default.reset(import_picocolors2.default.dim(`Press ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" space ")))} to select, ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" enter ")))} to submit`))}`;
2851
+ }, render() {
2852
+ const r2 = `${import_picocolors2.default.gray(o)}
2853
+ ${b2(this.state)} ${t.message}
2854
+ `, i = (s, c) => {
2855
+ const a = this.value.includes(s.value);
2856
+ return c && a ? n(s, "active-selected") : a ? n(s, "selected") : n(s, c ? "active" : "inactive");
2857
+ };
2858
+ switch (this.state) {
2859
+ case "submit":
2860
+ return `${r2}${import_picocolors2.default.gray(o)} ${this.options.filter(({ value: s }) => this.value.includes(s)).map((s) => n(s, "submitted")).join(import_picocolors2.default.dim(", ")) || import_picocolors2.default.dim("none")}`;
2861
+ case "cancel": {
2862
+ const s = this.options.filter(({ value: c }) => this.value.includes(c)).map((c) => n(c, "cancelled")).join(import_picocolors2.default.dim(", "));
2863
+ return `${r2}${import_picocolors2.default.gray(o)} ${s.trim() ? `${s}
2864
+ ${import_picocolors2.default.gray(o)}` : ""}`;
2865
+ }
2866
+ case "error": {
2867
+ const s = this.error.split(`
2868
+ `).map((c, a) => a === 0 ? `${import_picocolors2.default.yellow(d2)} ${import_picocolors2.default.yellow(c)}` : ` ${c}`).join(`
2869
+ `);
2870
+ return `${r2 + import_picocolors2.default.yellow(o)} ${G2({ options: this.options, cursor: this.cursor, maxItems: t.maxItems, style: i }).join(`
2871
+ ${import_picocolors2.default.yellow(o)} `)}
2872
+ ${s}
2873
+ `;
2874
+ }
2875
+ default:
2876
+ return `${r2}${import_picocolors2.default.cyan(o)} ${G2({ options: this.options, cursor: this.cursor, maxItems: t.maxItems, style: i }).join(`
2877
+ ${import_picocolors2.default.cyan(o)} `)}
2878
+ ${import_picocolors2.default.cyan(d2)}
2694
2879
  `;
2695
2880
  }
2696
2881
  } }).prompt();
2697
2882
  };
2698
2883
  var xe = (t = "") => {
2699
- process.stdout.write(`${import_picocolors.default.gray(d2)} ${import_picocolors.default.red(t)}
2884
+ process.stdout.write(`${import_picocolors2.default.gray(d2)} ${import_picocolors2.default.red(t)}
2700
2885
 
2701
2886
  `);
2702
2887
  };
2703
2888
  var Ie = (t = "") => {
2704
- process.stdout.write(`${import_picocolors.default.gray(ue)} ${t}
2889
+ process.stdout.write(`${import_picocolors2.default.gray(ue)} ${t}
2705
2890
  `);
2706
2891
  };
2707
2892
  var Se = (t = "") => {
2708
- process.stdout.write(`${import_picocolors.default.gray(o)}
2709
- ${import_picocolors.default.gray(d2)} ${t}
2893
+ process.stdout.write(`${import_picocolors2.default.gray(o)}
2894
+ ${import_picocolors2.default.gray(d2)} ${t}
2710
2895
 
2711
2896
  `);
2712
2897
  };
2713
- var M2 = { message: (t = "", { symbol: n = import_picocolors.default.gray(o) } = {}) => {
2714
- const r2 = [`${import_picocolors.default.gray(o)}`];
2898
+ var M2 = { message: (t = "", { symbol: n = import_picocolors2.default.gray(o) } = {}) => {
2899
+ const r2 = [`${import_picocolors2.default.gray(o)}`];
2715
2900
  if (t) {
2716
2901
  const [i, ...s] = t.split(`
2717
2902
  `);
2718
- r2.push(`${n} ${i}`, ...s.map((c) => `${import_picocolors.default.gray(o)} ${c}`));
2903
+ r2.push(`${n} ${i}`, ...s.map((c) => `${import_picocolors2.default.gray(o)} ${c}`));
2719
2904
  }
2720
2905
  process.stdout.write(`${r2.join(`
2721
2906
  `)}
2722
2907
  `);
2723
2908
  }, info: (t) => {
2724
- M2.message(t, { symbol: import_picocolors.default.blue(q) });
2909
+ M2.message(t, { symbol: import_picocolors2.default.blue(q) });
2725
2910
  }, success: (t) => {
2726
- M2.message(t, { symbol: import_picocolors.default.green(D) });
2911
+ M2.message(t, { symbol: import_picocolors2.default.green(D) });
2727
2912
  }, step: (t) => {
2728
- M2.message(t, { symbol: import_picocolors.default.green(C) });
2913
+ M2.message(t, { symbol: import_picocolors2.default.green(C) });
2729
2914
  }, warn: (t) => {
2730
- M2.message(t, { symbol: import_picocolors.default.yellow(U) });
2915
+ M2.message(t, { symbol: import_picocolors2.default.yellow(U) });
2731
2916
  }, warning: (t) => {
2732
2917
  M2.warn(t);
2733
2918
  }, error: (t) => {
2734
- M2.message(t, { symbol: import_picocolors.default.red(K2) });
2919
+ M2.message(t, { symbol: import_picocolors2.default.red(K2) });
2735
2920
  } };
2736
- var J = `${import_picocolors.default.gray(o)} `;
2921
+ var J2 = `${import_picocolors2.default.gray(o)} `;
2737
2922
  var Y2 = ({ indicator: t = "dots" } = {}) => {
2738
2923
  const n = V2 ? ["◒", "◐", "◓", "◑"] : ["•", "o", "O", "0"], r2 = V2 ? 80 : 120, i = process.env.CI === "true";
2739
2924
  let s, c, a = false, l2 = "", $2, g2 = performance.now();
2740
2925
  const p2 = (m2) => {
2741
2926
  const h2 = m2 > 1 ? "Something went wrong" : "Canceled";
2742
2927
  a && N2(h2, m2);
2743
- }, v = () => p2(2), f = () => p2(1), j2 = () => {
2744
- process.on("uncaughtExceptionMonitor", v), process.on("unhandledRejection", v), process.on("SIGINT", f), process.on("SIGTERM", f), process.on("exit", p2);
2928
+ }, v2 = () => p2(2), f = () => p2(1), j2 = () => {
2929
+ process.on("uncaughtExceptionMonitor", v2), process.on("unhandledRejection", v2), process.on("SIGINT", f), process.on("SIGTERM", f), process.on("exit", p2);
2745
2930
  }, E = () => {
2746
- process.removeListener("uncaughtExceptionMonitor", v), process.removeListener("unhandledRejection", v), process.removeListener("SIGINT", f), process.removeListener("SIGTERM", f), process.removeListener("exit", p2);
2931
+ process.removeListener("uncaughtExceptionMonitor", v2), process.removeListener("unhandledRejection", v2), process.removeListener("SIGINT", f), process.removeListener("SIGTERM", f), process.removeListener("exit", p2);
2747
2932
  }, B2 = () => {
2748
2933
  if ($2 === undefined)
2749
2934
  return;
@@ -2755,15 +2940,15 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
2755
2940
  }, R2 = (m2) => m2.replace(/\.+$/, ""), O2 = (m2) => {
2756
2941
  const h2 = (performance.now() - m2) / 1000, w2 = Math.floor(h2 / 60), I2 = Math.floor(h2 % 60);
2757
2942
  return w2 > 0 ? `[${w2}m ${I2}s]` : `[${I2}s]`;
2758
- }, H = (m2 = "") => {
2759
- a = true, s = fD(), l2 = R2(m2), g2 = performance.now(), process.stdout.write(`${import_picocolors.default.gray(o)}
2943
+ }, H2 = (m2 = "") => {
2944
+ a = true, s = fD(), l2 = R2(m2), g2 = performance.now(), process.stdout.write(`${import_picocolors2.default.gray(o)}
2760
2945
  `);
2761
2946
  let h2 = 0, w2 = 0;
2762
2947
  j2(), c = setInterval(() => {
2763
2948
  if (i && l2 === $2)
2764
2949
  return;
2765
2950
  B2(), $2 = l2;
2766
- const I2 = import_picocolors.default.magenta(n[h2]);
2951
+ const I2 = import_picocolors2.default.magenta(n[h2]);
2767
2952
  if (i)
2768
2953
  process.stdout.write(`${I2} ${l2}...`);
2769
2954
  else if (t === "timer")
@@ -2776,24 +2961,152 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
2776
2961
  }, r2);
2777
2962
  }, N2 = (m2 = "", h2 = 0) => {
2778
2963
  a = false, clearInterval(c), B2();
2779
- const w2 = h2 === 0 ? import_picocolors.default.green(C) : h2 === 1 ? import_picocolors.default.red(L2) : import_picocolors.default.red(W2);
2964
+ const w2 = h2 === 0 ? import_picocolors2.default.green(C) : h2 === 1 ? import_picocolors2.default.red(L2) : import_picocolors2.default.red(W2);
2780
2965
  l2 = R2(m2 ?? l2), t === "timer" ? process.stdout.write(`${w2} ${l2} ${O2(g2)}
2781
2966
  `) : process.stdout.write(`${w2} ${l2}
2782
2967
  `), E(), s();
2783
2968
  };
2784
- return { start: H, stop: N2, message: (m2 = "") => {
2969
+ return { start: H2, stop: N2, message: (m2 = "") => {
2785
2970
  l2 = R2(m2 ?? l2);
2786
2971
  } };
2787
2972
  };
2788
2973
 
2789
2974
  // src/cli/install.ts
2790
- var import_picocolors2 = __toESM(require_picocolors(), 1);
2975
+ var import_picocolors3 = __toESM(require_picocolors(), 1);
2791
2976
 
2792
2977
  // src/cli/config-manager.ts
2793
2978
  import { existsSync } from "node:fs";
2794
2979
  import { readFile, writeFile } from "node:fs/promises";
2795
2980
  import { join } from "node:path";
2981
+
2982
+ // node_modules/strip-json-comments/index.js
2983
+ var singleComment = Symbol("singleComment");
2984
+ var multiComment = Symbol("multiComment");
2985
+ var stripWithoutWhitespace = () => "";
2986
+ var stripWithWhitespace = (string, start, end) => string.slice(start, end).replace(/[^ \t\r\n]/g, " ");
2987
+ var isEscaped = (jsonString, quotePosition) => {
2988
+ let index = quotePosition - 1;
2989
+ let backslashCount = 0;
2990
+ while (jsonString[index] === "\\") {
2991
+ index -= 1;
2992
+ backslashCount += 1;
2993
+ }
2994
+ return Boolean(backslashCount % 2);
2995
+ };
2996
+ function stripJsonComments(jsonString, { whitespace = true, trailingCommas = false } = {}) {
2997
+ if (typeof jsonString !== "string") {
2998
+ throw new TypeError(`Expected argument \`jsonString\` to be a \`string\`, got \`${typeof jsonString}\``);
2999
+ }
3000
+ const strip = whitespace ? stripWithWhitespace : stripWithoutWhitespace;
3001
+ let isInsideString = false;
3002
+ let isInsideComment = false;
3003
+ let offset = 0;
3004
+ let buffer = "";
3005
+ let result = "";
3006
+ let commaIndex = -1;
3007
+ for (let index = 0;index < jsonString.length; index++) {
3008
+ const currentCharacter = jsonString[index];
3009
+ const nextCharacter = jsonString[index + 1];
3010
+ if (!isInsideComment && currentCharacter === '"') {
3011
+ const escaped = isEscaped(jsonString, index);
3012
+ if (!escaped) {
3013
+ isInsideString = !isInsideString;
3014
+ }
3015
+ }
3016
+ if (isInsideString) {
3017
+ continue;
3018
+ }
3019
+ if (!isInsideComment && currentCharacter + nextCharacter === "//") {
3020
+ buffer += jsonString.slice(offset, index);
3021
+ offset = index;
3022
+ isInsideComment = singleComment;
3023
+ index++;
3024
+ } else if (isInsideComment === singleComment && currentCharacter + nextCharacter === `\r
3025
+ `) {
3026
+ index++;
3027
+ isInsideComment = false;
3028
+ buffer += strip(jsonString, offset, index);
3029
+ offset = index;
3030
+ continue;
3031
+ } else if (isInsideComment === singleComment && currentCharacter === `
3032
+ `) {
3033
+ isInsideComment = false;
3034
+ buffer += strip(jsonString, offset, index);
3035
+ offset = index;
3036
+ } else if (!isInsideComment && currentCharacter + nextCharacter === "/*") {
3037
+ buffer += jsonString.slice(offset, index);
3038
+ offset = index;
3039
+ isInsideComment = multiComment;
3040
+ index++;
3041
+ continue;
3042
+ } else if (isInsideComment === multiComment && currentCharacter + nextCharacter === "*/") {
3043
+ index++;
3044
+ isInsideComment = false;
3045
+ buffer += strip(jsonString, offset, index + 1);
3046
+ offset = index + 1;
3047
+ continue;
3048
+ } else if (trailingCommas && !isInsideComment) {
3049
+ if (commaIndex !== -1) {
3050
+ if (currentCharacter === "}" || currentCharacter === "]") {
3051
+ buffer += jsonString.slice(offset, index);
3052
+ result += strip(buffer, 0, 1) + buffer.slice(1);
3053
+ buffer = "";
3054
+ offset = index;
3055
+ commaIndex = -1;
3056
+ } else if (currentCharacter !== " " && currentCharacter !== "\t" && currentCharacter !== "\r" && currentCharacter !== `
3057
+ `) {
3058
+ buffer += jsonString.slice(offset, index);
3059
+ offset = index;
3060
+ commaIndex = -1;
3061
+ }
3062
+ } else if (currentCharacter === ",") {
3063
+ result += buffer + jsonString.slice(offset, index);
3064
+ buffer = "";
3065
+ offset = index;
3066
+ commaIndex = index;
3067
+ }
3068
+ }
3069
+ }
3070
+ const remaining = isInsideComment === singleComment ? strip(jsonString, offset) : jsonString.slice(offset);
3071
+ return result + buffer + remaining;
3072
+ }
3073
+
3074
+ // src/cli/constants.ts
2796
3075
  var PACKAGE_NAME = "zenox";
3076
+ var AGENTS = [
3077
+ {
3078
+ name: "explorer",
3079
+ displayName: "Explorer",
3080
+ defaultModel: "anthropic/claude-haiku-4-5",
3081
+ description: "Fast codebase search specialist"
3082
+ },
3083
+ {
3084
+ name: "librarian",
3085
+ displayName: "Librarian",
3086
+ defaultModel: "anthropic/claude-sonnet-4-5",
3087
+ description: "Open-source research agent"
3088
+ },
3089
+ {
3090
+ name: "oracle",
3091
+ displayName: "Oracle",
3092
+ defaultModel: "openai/gpt-5.2",
3093
+ description: "Strategic technical advisor"
3094
+ },
3095
+ {
3096
+ name: "ui-planner",
3097
+ displayName: "UI-Planner",
3098
+ defaultModel: "google/gemini-3-pro-high",
3099
+ description: "Designer-turned-developer"
3100
+ }
3101
+ ];
3102
+ var DEFAULT_MODELS = {
3103
+ explorer: "anthropic/claude-haiku-4-5",
3104
+ librarian: "anthropic/claude-sonnet-4-5",
3105
+ oracle: "openai/gpt-5.2",
3106
+ "ui-planner": "google/gemini-3-pro-high"
3107
+ };
3108
+
3109
+ // src/cli/config-manager.ts
2797
3110
  var CONFIG_FILENAMES = ["opencode.json", "opencode.jsonc"];
2798
3111
  function findConfigFile(directory) {
2799
3112
  for (const filename of CONFIG_FILENAMES) {
@@ -2807,16 +3120,16 @@ function findConfigFile(directory) {
2807
3120
  }
2808
3121
  async function readConfig(configPath) {
2809
3122
  const content = await readFile(configPath, "utf-8");
2810
- const jsonContent = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
3123
+ const jsonContent = stripJsonComments(content, { trailingCommas: true });
2811
3124
  const config = JSON.parse(jsonContent);
2812
3125
  return { content, config };
2813
3126
  }
2814
3127
  function isPluginInstalled(config) {
2815
- const plugins = config.plugins ?? [];
3128
+ const plugins = config.plugins ?? config.plugin ?? [];
2816
3129
  return plugins.some((p2) => p2 === PACKAGE_NAME || p2.startsWith(`${PACKAGE_NAME}@`));
2817
3130
  }
2818
3131
  function addPluginToJsonc(content) {
2819
- const pluginsMatch = content.match(/"plugins"\s*:\s*\[/);
3132
+ const pluginsMatch = content.match(/"plugins"\s*:\s*\[/) ?? content.match(/"plugin"\s*:\s*\[/);
2820
3133
  if (pluginsMatch) {
2821
3134
  const insertIndex = content.indexOf("[", pluginsMatch.index) + 1;
2822
3135
  const beforeBracket = content.slice(0, insertIndex);
@@ -2841,14 +3154,17 @@ function addPluginToJsonc(content) {
2841
3154
  }`;
2842
3155
  } else {
2843
3156
  return `${beforeBrace}
2844
- "plugins": ["${PACKAGE_NAME}"],${afterBrace}`;
3157
+ "plugins": ["${PACKAGE_NAME}"],
3158
+ ${afterBrace.trimStart()}`;
2845
3159
  }
2846
3160
  }
2847
3161
  }
2848
3162
  function addPluginToJson(config) {
3163
+ const existingKey = config.plugins ? "plugins" : config.plugin ? "plugin" : "plugins";
3164
+ const existingPlugins = config.plugins ?? config.plugin ?? [];
2849
3165
  const newConfig = {
2850
- plugins: [PACKAGE_NAME, ...config.plugins ?? []],
2851
- ...Object.fromEntries(Object.entries(config).filter(([key]) => key !== "plugins"))
3166
+ [existingKey]: [PACKAGE_NAME, ...existingPlugins],
3167
+ ...Object.fromEntries(Object.entries(config).filter(([key]) => key !== "plugins" && key !== "plugin"))
2852
3168
  };
2853
3169
  return JSON.stringify(newConfig, null, 2) + `
2854
3170
  `;
@@ -2873,8 +3189,189 @@ async function createDefaultConfig(configPath) {
2873
3189
  `, "utf-8");
2874
3190
  }
2875
3191
 
3192
+ // src/cli/zenox-config.ts
3193
+ import { existsSync as existsSync2 } from "node:fs";
3194
+ import { readFile as readFile2, writeFile as writeFile2, mkdir } from "node:fs/promises";
3195
+ import { join as join2, dirname } from "node:path";
3196
+ import { homedir } from "node:os";
3197
+ function getZenoxConfigPath() {
3198
+ return join2(homedir(), ".config", "opencode", "zenox.json");
3199
+ }
3200
+ async function readZenoxConfig() {
3201
+ const configPath = getZenoxConfigPath();
3202
+ if (!existsSync2(configPath))
3203
+ return null;
3204
+ try {
3205
+ const content = await readFile2(configPath, "utf-8");
3206
+ return JSON.parse(content);
3207
+ } catch {
3208
+ return null;
3209
+ }
3210
+ }
3211
+ async function writeZenoxConfig(config) {
3212
+ const configPath = getZenoxConfigPath();
3213
+ const configDir = dirname(configPath);
3214
+ if (!existsSync2(configDir)) {
3215
+ await mkdir(configDir, { recursive: true });
3216
+ }
3217
+ await writeFile2(configPath, JSON.stringify(config, null, 2) + `
3218
+ `, "utf-8");
3219
+ }
3220
+ async function updateAgentModels(models) {
3221
+ const existing = await readZenoxConfig() ?? {};
3222
+ const agents = { ...existing.agents };
3223
+ for (const [agent, model] of Object.entries(models)) {
3224
+ if (model) {
3225
+ agents[agent] = { model };
3226
+ }
3227
+ }
3228
+ await writeZenoxConfig({
3229
+ ...existing,
3230
+ agents
3231
+ });
3232
+ }
3233
+ function getCurrentModels(config) {
3234
+ if (!config?.agents)
3235
+ return {};
3236
+ const result = {};
3237
+ for (const [agent, settings] of Object.entries(config.agents)) {
3238
+ if (settings?.model) {
3239
+ result[agent] = settings.model;
3240
+ }
3241
+ }
3242
+ return result;
3243
+ }
3244
+
3245
+ // src/cli/model-picker.ts
3246
+ var CUSTOM_MODEL_OPTION = "__custom__";
3247
+ function extractUserModels(config) {
3248
+ const models = new Set;
3249
+ const provider = config.provider;
3250
+ if (!provider)
3251
+ return [];
3252
+ for (const providerConfig of Object.values(provider)) {
3253
+ if (typeof providerConfig !== "object" || providerConfig === null)
3254
+ continue;
3255
+ const providerModels = providerConfig.models;
3256
+ if (typeof providerModels !== "object" || providerModels === null)
3257
+ continue;
3258
+ for (const modelId of Object.keys(providerModels)) {
3259
+ models.add(modelId);
3260
+ }
3261
+ }
3262
+ return Array.from(models).sort();
3263
+ }
3264
+ function buildModelOptions(agent, userModels, currentModel) {
3265
+ const options = [];
3266
+ const isDefault = !currentModel || currentModel === agent.defaultModel;
3267
+ options.push({
3268
+ value: agent.defaultModel,
3269
+ label: `${agent.defaultModel}${isDefault ? " (Current)" : ""}`,
3270
+ hint: "Recommended - Default"
3271
+ });
3272
+ for (const model of userModels) {
3273
+ if (model === agent.defaultModel)
3274
+ continue;
3275
+ const isCurrent = currentModel === model;
3276
+ options.push({
3277
+ value: model,
3278
+ label: `${model}${isCurrent ? " (Current)" : ""}`,
3279
+ hint: "From your config"
3280
+ });
3281
+ }
3282
+ if (currentModel && currentModel !== agent.defaultModel && !userModels.includes(currentModel)) {
3283
+ options.push({
3284
+ value: currentModel,
3285
+ label: `${currentModel} (Current)`,
3286
+ hint: "Custom"
3287
+ });
3288
+ }
3289
+ options.push({
3290
+ value: CUSTOM_MODEL_OPTION,
3291
+ label: "Enter custom model name",
3292
+ hint: "Get names from models.dev"
3293
+ });
3294
+ return options;
3295
+ }
3296
+ async function pickModelForAgent(agent, userModels, currentModel) {
3297
+ const options = buildModelOptions(agent, userModels, currentModel);
3298
+ const selected = await ve({
3299
+ message: `${agent.displayName} agent model:`,
3300
+ options
3301
+ });
3302
+ if (pD(selected))
3303
+ return null;
3304
+ if (selected === CUSTOM_MODEL_OPTION) {
3305
+ const customModel = await he({
3306
+ message: "Enter custom model name:",
3307
+ placeholder: "e.g., anthropic/claude-sonnet-4 (see models.dev)",
3308
+ validate: (value) => {
3309
+ if (!value.trim())
3310
+ return "Model name is required";
3311
+ if (!value.includes("/"))
3312
+ return "Model should be in format: provider/model-name";
3313
+ return;
3314
+ }
3315
+ });
3316
+ if (pD(customModel))
3317
+ return null;
3318
+ return customModel;
3319
+ }
3320
+ return selected;
3321
+ }
3322
+ async function pickModelsForAllAgents(userModels, currentModels) {
3323
+ const result = {};
3324
+ for (const agent of AGENTS) {
3325
+ const current = currentModels[agent.name];
3326
+ const model = await pickModelForAgent(agent, userModels, current);
3327
+ if (model === null)
3328
+ return null;
3329
+ if (model !== agent.defaultModel) {
3330
+ result[agent.name] = model;
3331
+ }
3332
+ }
3333
+ return result;
3334
+ }
3335
+ async function askConfigureModels() {
3336
+ const result = await ve({
3337
+ message: "Configure sub-agent models?",
3338
+ options: [
3339
+ {
3340
+ value: "defaults",
3341
+ label: "Use recommended defaults",
3342
+ hint: "No config file needed"
3343
+ },
3344
+ {
3345
+ value: "customize",
3346
+ label: "Customize models",
3347
+ hint: "Choose models for each agent"
3348
+ }
3349
+ ]
3350
+ });
3351
+ if (pD(result))
3352
+ return null;
3353
+ return result;
3354
+ }
3355
+ async function selectAgentsToReconfigure(currentModels) {
3356
+ const options = AGENTS.map((agent) => {
3357
+ const current = currentModels[agent.name] ?? agent.defaultModel;
3358
+ return {
3359
+ value: agent.name,
3360
+ label: agent.displayName,
3361
+ hint: `current: ${current}`
3362
+ };
3363
+ });
3364
+ const selected = await fe({
3365
+ message: "Which agents to configure?",
3366
+ options,
3367
+ required: true
3368
+ });
3369
+ if (pD(selected))
3370
+ return null;
3371
+ return selected;
3372
+ }
3373
+
2876
3374
  // src/cli/install.ts
2877
- var PACKAGE_NAME2 = "zenox";
2878
3375
  async function runInstall(options = {}) {
2879
3376
  const { noTui = false, configPath: customConfigPath } = options;
2880
3377
  const cwd = process.cwd();
@@ -2885,8 +3382,9 @@ async function runInstall(options = {}) {
2885
3382
  await runInteractive(cwd, customConfigPath);
2886
3383
  }
2887
3384
  async function runInteractive(cwd, customConfigPath) {
2888
- Ie(import_picocolors2.default.cyan(`${PACKAGE_NAME2} installer`));
3385
+ Ie(import_picocolors3.default.cyan(`${PACKAGE_NAME} installer`));
2889
3386
  const configFile = customConfigPath ? { path: customConfigPath, format: "json" } : findConfigFile(cwd);
3387
+ let opencodeConfig = null;
2890
3388
  if (!configFile) {
2891
3389
  const shouldCreate = await ye({
2892
3390
  message: "No opencode.json found. Create one?",
@@ -2897,73 +3395,178 @@ async function runInteractive(cwd, customConfigPath) {
2897
3395
  process.exit(0);
2898
3396
  }
2899
3397
  const configPath = getDefaultConfigPath(cwd);
2900
- const spinner2 = Y2();
2901
- spinner2.start("Creating opencode.json");
3398
+ const spinner = Y2();
3399
+ spinner.start("Creating opencode.json");
2902
3400
  try {
2903
3401
  await createDefaultConfig(configPath);
2904
- spinner2.stop("Created opencode.json");
2905
- Se(import_picocolors2.default.green(`${PACKAGE_NAME2} installed successfully!`));
3402
+ spinner.stop("Created opencode.json");
2906
3403
  } catch (err) {
2907
- spinner2.stop("Failed to create config");
3404
+ spinner.stop("Failed to create config");
2908
3405
  M2.error(err instanceof Error ? err.message : "Unknown error");
2909
3406
  process.exit(1);
2910
3407
  }
2911
- return;
2912
- }
2913
- try {
2914
- const { config } = await readConfig(configFile.path);
2915
- if (isPluginInstalled(config)) {
2916
- M2.warn(`${PACKAGE_NAME2} is already installed in ${configFile.path}`);
2917
- Se(import_picocolors2.default.yellow("No changes made"));
2918
- return;
3408
+ } else {
3409
+ try {
3410
+ const result = await readConfig(configFile.path);
3411
+ opencodeConfig = result.config;
3412
+ if (isPluginInstalled(opencodeConfig)) {
3413
+ M2.info(`${PACKAGE_NAME} is already in ${configFile.path}`);
3414
+ } else {
3415
+ const shouldInstall = await ye({
3416
+ message: `Add ${PACKAGE_NAME} to ${configFile.path}?`,
3417
+ initialValue: true
3418
+ });
3419
+ if (pD(shouldInstall) || !shouldInstall) {
3420
+ xe("Installation cancelled");
3421
+ process.exit(0);
3422
+ }
3423
+ const spinner = Y2();
3424
+ spinner.start(`Adding ${PACKAGE_NAME} to plugins`);
3425
+ try {
3426
+ await installPlugin(configFile.path);
3427
+ spinner.stop(`Added ${PACKAGE_NAME} to plugins`);
3428
+ } catch (err) {
3429
+ spinner.stop("Failed to install");
3430
+ M2.error(err instanceof Error ? err.message : "Unknown error");
3431
+ process.exit(1);
3432
+ }
3433
+ }
3434
+ } catch (err) {
3435
+ M2.error(`Failed to read config: ${err instanceof Error ? err.message : "Unknown error"}`);
3436
+ process.exit(1);
2919
3437
  }
2920
- } catch (err) {
2921
- M2.error(`Failed to read config: ${err instanceof Error ? err.message : "Unknown error"}`);
2922
- process.exit(1);
2923
3438
  }
2924
- const shouldInstall = await ye({
2925
- message: `Add ${PACKAGE_NAME2} to ${configFile.path}?`,
2926
- initialValue: true
2927
- });
2928
- if (pD(shouldInstall) || !shouldInstall) {
3439
+ const configureChoice = await askConfigureModels();
3440
+ if (configureChoice === null) {
2929
3441
  xe("Installation cancelled");
2930
3442
  process.exit(0);
2931
3443
  }
2932
- const spinner = Y2();
2933
- spinner.start(`Adding ${PACKAGE_NAME2} to plugins`);
2934
- try {
2935
- await installPlugin(configFile.path);
2936
- spinner.stop(`Added ${PACKAGE_NAME2} to plugins`);
2937
- Se(import_picocolors2.default.green(`${PACKAGE_NAME2} installed successfully!`));
2938
- } catch (err) {
2939
- spinner.stop("Failed to install");
2940
- M2.error(err instanceof Error ? err.message : "Unknown error");
2941
- process.exit(1);
3444
+ if (configureChoice === "customize") {
3445
+ const userModels = opencodeConfig ? extractUserModels(opencodeConfig) : [];
3446
+ const zenoxConfig = await readZenoxConfig();
3447
+ const currentModels = getCurrentModels(zenoxConfig);
3448
+ const selectedModels = await pickModelsForAllAgents(userModels, currentModels);
3449
+ if (selectedModels === null) {
3450
+ xe("Installation cancelled");
3451
+ process.exit(0);
3452
+ }
3453
+ if (Object.keys(selectedModels).length > 0) {
3454
+ const spinner = Y2();
3455
+ spinner.start("Saving model configuration");
3456
+ try {
3457
+ await updateAgentModels(selectedModels);
3458
+ spinner.stop(`Created ${getZenoxConfigPath()}`);
3459
+ } catch (err) {
3460
+ spinner.stop("Failed to save config");
3461
+ M2.error(err instanceof Error ? err.message : "Unknown error");
3462
+ process.exit(1);
3463
+ }
3464
+ } else {
3465
+ M2.info("Using default models - no zenox.json needed");
3466
+ }
2942
3467
  }
3468
+ Se(import_picocolors3.default.green(`${PACKAGE_NAME} installed successfully!`));
2943
3469
  }
2944
3470
  async function runNonInteractive(cwd, customConfigPath) {
2945
3471
  const configFile = customConfigPath ? { path: customConfigPath, format: "json" } : findConfigFile(cwd);
2946
3472
  if (!configFile) {
2947
- console.log(`Creating opencode.json with ${PACKAGE_NAME2}...`);
3473
+ console.log(`Creating opencode.json with ${PACKAGE_NAME}...`);
2948
3474
  const configPath = getDefaultConfigPath(cwd);
2949
3475
  await createDefaultConfig(configPath);
2950
- console.log(`Created opencode.json with ${PACKAGE_NAME2}`);
3476
+ console.log(`Created opencode.json with ${PACKAGE_NAME}`);
3477
+ console.log("Using default models (run 'bunx zenox config' to customize)");
2951
3478
  return;
2952
3479
  }
2953
3480
  const { config } = await readConfig(configFile.path);
2954
3481
  if (isPluginInstalled(config)) {
2955
- console.log(`${PACKAGE_NAME2} is already installed`);
3482
+ console.log(`${PACKAGE_NAME} is already installed`);
2956
3483
  return;
2957
3484
  }
2958
- console.log(`Adding ${PACKAGE_NAME2} to ${configFile.path}...`);
3485
+ console.log(`Adding ${PACKAGE_NAME} to ${configFile.path}...`);
2959
3486
  await installPlugin(configFile.path);
2960
- console.log(`${PACKAGE_NAME2} installed successfully!`);
3487
+ console.log(`${PACKAGE_NAME} installed successfully!`);
3488
+ console.log("Using default models (run 'bunx zenox config' to customize)");
3489
+ }
3490
+
3491
+ // src/cli/config.ts
3492
+ var import_picocolors4 = __toESM(require_picocolors(), 1);
3493
+ async function runConfig() {
3494
+ Ie(import_picocolors4.default.cyan(`${PACKAGE_NAME} model configurator`));
3495
+ const cwd = process.cwd();
3496
+ let userModels = [];
3497
+ const configFile = findConfigFile(cwd);
3498
+ if (configFile) {
3499
+ try {
3500
+ const { config } = await readConfig(configFile.path);
3501
+ userModels = extractUserModels(config);
3502
+ } catch {}
3503
+ }
3504
+ const zenoxConfig = await readZenoxConfig();
3505
+ const currentModels = getCurrentModels(zenoxConfig);
3506
+ M2.info("Current model configuration:");
3507
+ for (const agent of AGENTS) {
3508
+ const model = currentModels[agent.name] ?? agent.defaultModel;
3509
+ const isCustom = currentModels[agent.name] !== undefined;
3510
+ const suffix = isCustom ? import_picocolors4.default.yellow(" (custom)") : import_picocolors4.default.dim(" (default)");
3511
+ M2.message(` ${agent.displayName}: ${model}${suffix}`);
3512
+ }
3513
+ const selectedAgents = await selectAgentsToReconfigure(currentModels);
3514
+ if (selectedAgents === null) {
3515
+ xe("Configuration cancelled");
3516
+ process.exit(0);
3517
+ }
3518
+ if (selectedAgents.length === 0) {
3519
+ M2.warn("No agents selected");
3520
+ Se(import_picocolors4.default.yellow("No changes made"));
3521
+ return;
3522
+ }
3523
+ const newModels = {};
3524
+ for (const agentName of selectedAgents) {
3525
+ const agent = AGENTS.find((a) => a.name === agentName);
3526
+ const current = currentModels[agentName];
3527
+ const model = await pickModelForAgent(agent, userModels, current);
3528
+ if (model === null) {
3529
+ xe("Configuration cancelled");
3530
+ process.exit(0);
3531
+ }
3532
+ if (model !== agent.defaultModel) {
3533
+ newModels[agentName] = model;
3534
+ } else {
3535
+ newModels[agentName] = model;
3536
+ }
3537
+ }
3538
+ const finalModels = {};
3539
+ for (const [agent, model] of Object.entries(currentModels)) {
3540
+ if (!selectedAgents.includes(agent)) {
3541
+ finalModels[agent] = model;
3542
+ }
3543
+ }
3544
+ for (const [agent, model] of Object.entries(newModels)) {
3545
+ if (model !== DEFAULT_MODELS[agent]) {
3546
+ finalModels[agent] = model;
3547
+ }
3548
+ }
3549
+ if (Object.keys(finalModels).length > 0) {
3550
+ const spinner = Y2();
3551
+ spinner.start("Saving model configuration");
3552
+ try {
3553
+ await updateAgentModels(finalModels);
3554
+ spinner.stop(`Updated ${getZenoxConfigPath()}`);
3555
+ } catch (err) {
3556
+ spinner.stop("Failed to save config");
3557
+ M2.error(err instanceof Error ? err.message : "Unknown error");
3558
+ process.exit(1);
3559
+ }
3560
+ } else {
3561
+ M2.info("All agents using default models - zenox.json not needed");
3562
+ }
3563
+ Se(import_picocolors4.default.green("Configuration updated!"));
2961
3564
  }
2962
3565
 
2963
3566
  // src/cli/index.ts
2964
3567
  var program2 = new Command;
2965
- program2.name("zenox").description("Zenox - OpenCode plugin for intelligent agent orchestration").version("0.1.0");
2966
- program2.command("install").description("Add zenox to your opencode.json plugins").option("--no-tui", "Run in non-interactive mode").option("-c, --config <path>", "Path to opencode.json").action(async (options) => {
3568
+ program2.name("zenox").description("Zenox - OpenCode plugin for intelligent agent orchestration").version("1.0.0");
3569
+ program2.command("install").description("Add zenox to your opencode.json plugins and configure models").option("--no-tui", "Run in non-interactive mode (uses default models)").option("-c, --config <path>", "Path to opencode.json").action(async (options) => {
2967
3570
  try {
2968
3571
  await runInstall({
2969
3572
  noTui: !options.tui,
@@ -2974,4 +3577,12 @@ program2.command("install").description("Add zenox to your opencode.json plugins
2974
3577
  process.exit(1);
2975
3578
  }
2976
3579
  });
3580
+ program2.command("config").alias("models").description("Reconfigure sub-agent models").action(async () => {
3581
+ try {
3582
+ await runConfig();
3583
+ } catch (err) {
3584
+ console.error(err instanceof Error ? err.message : "Unknown error");
3585
+ process.exit(1);
3586
+ }
3587
+ });
2977
3588
  program2.parse();
@@ -0,0 +1,7 @@
1
+ import type { OpencodeConfig } from "./types";
2
+ import type { AgentName, AgentInfo } from "./constants";
3
+ export declare function extractUserModels(config: OpencodeConfig): string[];
4
+ export declare function pickModelForAgent(agent: AgentInfo, userModels: string[], currentModel?: string): Promise<string | null>;
5
+ export declare function pickModelsForAllAgents(userModels: string[], currentModels: Partial<Record<AgentName, string>>): Promise<Partial<Record<AgentName, string>> | null>;
6
+ export declare function askConfigureModels(): Promise<"defaults" | "customize" | null>;
7
+ export declare function selectAgentsToReconfigure(currentModels: Partial<Record<AgentName, string>>): Promise<AgentName[] | null>;
@@ -1,5 +1,6 @@
1
1
  export interface OpencodeConfig {
2
2
  plugins?: string[];
3
+ plugin?: string[];
3
4
  [key: string]: unknown;
4
5
  }
5
6
  export interface InstallOptions {
@@ -0,0 +1,16 @@
1
+ import type { AgentName } from "./constants";
2
+ export interface ZenoxConfigAgents {
3
+ [key: string]: {
4
+ model: string;
5
+ };
6
+ }
7
+ export interface ZenoxConfig {
8
+ agents?: ZenoxConfigAgents;
9
+ disabled_agents?: string[];
10
+ disabled_mcps?: string[];
11
+ }
12
+ export declare function getZenoxConfigPath(): string;
13
+ export declare function readZenoxConfig(): Promise<ZenoxConfig | null>;
14
+ export declare function writeZenoxConfig(config: ZenoxConfig): Promise<void>;
15
+ export declare function updateAgentModels(models: Partial<Record<AgentName, string>>): Promise<void>;
16
+ export declare function getCurrentModels(config: ZenoxConfig | null): Partial<Record<AgentName, string>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zenox",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "OpenCode plugin with specialized agents (explorer, librarian, oracle, ui-planner), background tasks for parallel execution, and smart orchestration",
5
5
  "author": "Ayush",
6
6
  "license": "MIT",
@@ -54,6 +54,7 @@
54
54
  "@opencode-ai/sdk": "^1.1.12",
55
55
  "commander": "^14.0.0",
56
56
  "picocolors": "^1.1.1",
57
+ "strip-json-comments": "^5.0.3",
57
58
  "zod": "^3.24.1"
58
59
  },
59
60
  "devDependencies": {