opencode-agent-modes 0.2.0 → 0.3.1

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
@@ -41,7 +41,7 @@ Add the plugin to your `opencode.json`:
41
41
  ```
42
42
 
43
43
  The following command files are automatically copied to
44
- `~/.config/opencode/command/` when the plugin initializes:
44
+ `~/.config/opencode/commands/` when the plugin initializes:
45
45
 
46
46
  - `mode-performance.md`
47
47
  - `mode-economy.md`
@@ -135,7 +135,7 @@ To add a custom preset (e.g., "premium"):
135
135
  }
136
136
  ```
137
137
 
138
- 2. Create a command file at `~/.config/opencode/command/mode-premium.md`:
138
+ 2. Create a command file at `~/.config/opencode/commands/mode-premium.md`:
139
139
 
140
140
  ```md
141
141
  ---
@@ -150,6 +150,10 @@ To add a custom preset (e.g., "premium"):
150
150
  > [!INFO]
151
151
  > - Changes require an opencode restart to take effect
152
152
  > - Custom mode presets can be added by editing the configuration file
153
+ > - Built-in command files (`mode-performance.md`, `mode-economy.md`, etc.)
154
+ > are overwritten on every plugin startup. Do not modify them directly.
155
+ > - Custom command files (e.g., `mode-premium.md`) are not affected by
156
+ > this overwrite and will persist across restarts.
153
157
 
154
158
  ## Development
155
159
 
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Type guard utilities for configuration processing.
3
+ */
4
+ /**
5
+ * Checks if a value is a plain object (not null, not array).
6
+ *
7
+ * This is used throughout the codebase to distinguish between
8
+ * hierarchical structures and leaf values when processing
9
+ * configurations.
10
+ *
11
+ * @param obj - Value to check
12
+ * @returns True if the value is a plain object
13
+ */
14
+ export declare function isObject(obj: unknown): obj is Record<string, unknown>;
@@ -1,4 +1,5 @@
1
1
  export * from './types.ts';
2
+ export * from './guards.ts';
2
3
  export * from './loader.ts';
3
4
  export * from './initializer.ts';
4
5
  export * from './command-installer.ts';
@@ -1,17 +1,38 @@
1
1
  /**
2
- * Agent preset configuration for a single agent
2
+ * Generic model configuration for any agent.
3
+ *
4
+ * This interface is intentionally flexible to support arbitrary
5
+ * properties beyond just `model` and `variant`.
3
6
  */
4
- export interface AgentPreset {
5
- model: string;
7
+ export interface ModelConfig {
8
+ model?: string;
9
+ variant?: string;
10
+ [key: string]: unknown;
6
11
  }
7
12
  /**
8
- * Mode preset containing configurations for both opencode and oh-my-opencode agents
13
+ * Hierarchical preset structure supporting arbitrary nesting.
14
+ *
15
+ * This type recursively represents the configuration structure for
16
+ * both opencode and oh-my-opencode, supporting any level of nesting
17
+ * (e.g., agents/categories, future sections).
18
+ *
19
+ * Note: This type alias has a circular reference by design to support
20
+ * recursive structures. The TypeScript compiler warning about this
21
+ * can be safely ignored.
22
+ */
23
+ export type HierarchicalPreset = Record<string, ModelConfig | HierarchicalPreset>;
24
+ /**
25
+ * Mode preset containing configurations for both opencode and oh-my-opencode agents.
26
+ *
27
+ * Both opencode and oh-my-opencode use the same HierarchicalPreset type,
28
+ * allowing them to have arbitrary nested structures that are handled
29
+ * uniformly by recursive merge functions.
9
30
  */
10
31
  export interface ModePreset {
11
32
  description: string;
12
33
  model?: string;
13
- opencode: Record<string, AgentPreset>;
14
- 'oh-my-opencode': Record<string, AgentPreset>;
34
+ opencode: HierarchicalPreset;
35
+ 'oh-my-opencode': HierarchicalPreset;
15
36
  }
16
37
  /**
17
38
  * Main configuration for the mode switcher plugin
@@ -23,6 +44,9 @@ export interface ModeSwitcherConfig {
23
44
  }
24
45
  /**
25
46
  * OpenCode agent configuration structure in opencode.json
47
+ *
48
+ * @deprecated This type is kept for backward compatibility but may not
49
+ * accurately represent the actual structure. Use ModelConfig directly.
26
50
  */
27
51
  export interface OpencodeAgentConfig {
28
52
  model?: string;
@@ -31,17 +55,21 @@ export interface OpencodeAgentConfig {
31
55
  }
32
56
  /**
33
57
  * OpenCode configuration file structure
58
+ *
59
+ * Supports arbitrary properties beyond the documented ones.
34
60
  */
35
61
  export interface OpencodeConfig {
36
62
  model?: string;
37
- agent?: Record<string, OpencodeAgentConfig>;
63
+ agent?: HierarchicalPreset;
38
64
  [key: string]: unknown;
39
65
  }
40
66
  /**
41
67
  * Oh-my-opencode configuration file structure
68
+ *
69
+ * Supports arbitrary properties and nested structures like
70
+ * agents, categories, and any future sections.
42
71
  */
43
72
  export interface OhMyOpencodeConfig {
44
- agents?: Record<string, AgentPreset>;
45
73
  [key: string]: unknown;
46
74
  }
47
75
  /**
package/dist/index.js CHANGED
@@ -1,12 +1,16 @@
1
1
  // @bun
2
2
  var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
3
7
  var __export = (target, all) => {
4
8
  for (var name in all)
5
9
  __defProp(target, name, {
6
10
  get: all[name],
7
11
  enumerable: true,
8
12
  configurable: true,
9
- set: (newValue) => all[name] = () => newValue
13
+ set: __exportSetter.bind(all, name)
10
14
  });
11
15
  };
12
16
 
@@ -12332,6 +12336,10 @@ function tool(input) {
12332
12336
  tool.schema = exports_external;
12333
12337
  // src/config/types.ts
12334
12338
  var DEFAULT_ECONOMY_MODEL = "opencode/glm-4.7-free";
12339
+ // src/config/guards.ts
12340
+ function isObject2(obj) {
12341
+ return typeof obj === "object" && obj !== null && !Array.isArray(obj);
12342
+ }
12335
12343
  // src/config/loader.ts
12336
12344
  import { homedir } from "os";
12337
12345
  import { join } from "path";
@@ -13795,35 +13803,29 @@ async function pluginConfigExists() {
13795
13803
  return Bun.file(getPluginConfigPath()).exists();
13796
13804
  }
13797
13805
  // src/config/initializer.ts
13798
- var OPENCODE_AGENTS = [
13799
- "build",
13800
- "plan",
13801
- "summary",
13802
- "compaction",
13803
- "title",
13804
- "explore",
13805
- "general"
13806
- ];
13807
- async function buildPerformancePreset() {
13808
- const opencodeConfig = await loadOpencodeConfig();
13809
- const ohMyOpencodeConfig = await loadOhMyOpencodeConfig();
13810
- const opencodePreset = {};
13811
- const ohMyOpencodePreset = {};
13812
- if (opencodeConfig?.agent) {
13813
- for (const agentName of OPENCODE_AGENTS) {
13814
- const agentConfig = opencodeConfig.agent[agentName];
13815
- if (agentConfig?.model) {
13816
- opencodePreset[agentName] = { model: agentConfig.model };
13817
- }
13818
- }
13819
- }
13820
- if (ohMyOpencodeConfig?.agents) {
13821
- for (const [agentName, agentConfig] of Object.entries(ohMyOpencodeConfig.agents)) {
13822
- if (agentConfig?.model) {
13823
- ohMyOpencodePreset[agentName] = { model: agentConfig.model };
13806
+ function applyEconomyModel(config2, economyModel) {
13807
+ const result = {};
13808
+ for (const [key, value] of Object.entries(config2)) {
13809
+ if (isObject2(value)) {
13810
+ if ("model" in value) {
13811
+ result[key] = {
13812
+ ...value,
13813
+ model: economyModel
13814
+ };
13815
+ } else {
13816
+ result[key] = applyEconomyModel(value, economyModel);
13824
13817
  }
13818
+ } else {
13819
+ result[key] = value;
13825
13820
  }
13826
13821
  }
13822
+ return result;
13823
+ }
13824
+ async function buildPerformancePreset() {
13825
+ const opencodeConfig = await loadOpencodeConfig();
13826
+ const ohMyOpencodeConfig = await loadOhMyOpencodeConfig();
13827
+ const opencodePreset = opencodeConfig?.agent || {};
13828
+ const ohMyOpencodePreset = ohMyOpencodeConfig || {};
13827
13829
  const globalModel = opencodeConfig?.model;
13828
13830
  return {
13829
13831
  description: "High-performance models for complex tasks",
@@ -13835,22 +13837,8 @@ async function buildPerformancePreset() {
13835
13837
  async function buildEconomyPreset() {
13836
13838
  const opencodeConfig = await loadOpencodeConfig();
13837
13839
  const ohMyOpencodeConfig = await loadOhMyOpencodeConfig();
13838
- const opencodePreset = {};
13839
- const ohMyOpencodePreset = {};
13840
- if (opencodeConfig?.agent) {
13841
- for (const agentName of Object.keys(opencodeConfig.agent)) {
13842
- opencodePreset[agentName] = { model: DEFAULT_ECONOMY_MODEL };
13843
- }
13844
- } else {
13845
- for (const agentName of OPENCODE_AGENTS) {
13846
- opencodePreset[agentName] = { model: DEFAULT_ECONOMY_MODEL };
13847
- }
13848
- }
13849
- if (ohMyOpencodeConfig?.agents) {
13850
- for (const agentName of Object.keys(ohMyOpencodeConfig.agents)) {
13851
- ohMyOpencodePreset[agentName] = { model: DEFAULT_ECONOMY_MODEL };
13852
- }
13853
- }
13840
+ const opencodePreset = applyEconomyModel(opencodeConfig?.agent || {}, DEFAULT_ECONOMY_MODEL);
13841
+ const ohMyOpencodePreset = applyEconomyModel(ohMyOpencodeConfig || {}, DEFAULT_ECONOMY_MODEL);
13854
13842
  return {
13855
13843
  description: "Cost-efficient free model for routine tasks",
13856
13844
  model: DEFAULT_ECONOMY_MODEL,
@@ -13884,7 +13872,7 @@ import { copyFileSync, existsSync, mkdirSync, readdirSync } from "fs";
13884
13872
  import { homedir as homedir2 } from "os";
13885
13873
  import { dirname, join as join2 } from "path";
13886
13874
  import { fileURLToPath } from "url";
13887
- var COMMANDS_DEST = join2(homedir2(), ".config", "opencode", "command");
13875
+ var COMMANDS_DEST = join2(homedir2(), ".config", "opencode", "commands");
13888
13876
  function findCommandsDir() {
13889
13877
  const __dirname2 = dirname(fileURLToPath(import.meta.url));
13890
13878
  const candidates = [
@@ -13911,6 +13899,69 @@ function copyCommandFiles() {
13911
13899
  }
13912
13900
  }
13913
13901
  // src/modes/manager.ts
13902
+ function isLeafNode(value) {
13903
+ return "model" in value && typeof value.model === "string";
13904
+ }
13905
+ function deepMergeModel(target, preset) {
13906
+ for (const [key, value] of Object.entries(preset)) {
13907
+ if (!isObject2(value))
13908
+ continue;
13909
+ const actualValue = target[key];
13910
+ if (isLeafNode(value)) {
13911
+ const valueRecord = value;
13912
+ const existing = actualValue ?? {};
13913
+ const merged = {
13914
+ ...existing,
13915
+ ...valueRecord
13916
+ };
13917
+ target[key] = merged;
13918
+ } else {
13919
+ const childTarget = actualValue ?? {};
13920
+ target[key] = childTarget;
13921
+ deepMergeModel(childTarget, value);
13922
+ }
13923
+ }
13924
+ }
13925
+ function hasDriftRecursive(actual, expected) {
13926
+ for (const [key, expectedValue] of Object.entries(expected)) {
13927
+ if (!isObject2(expectedValue))
13928
+ continue;
13929
+ const actualValue = actual[key];
13930
+ if (isLeafNode(expectedValue)) {
13931
+ const actualObj = actualValue;
13932
+ if (!actualObj) {
13933
+ return true;
13934
+ }
13935
+ for (const [propKey, expectedPropValue] of Object.entries(expectedValue)) {
13936
+ if (actualObj[propKey] !== expectedPropValue) {
13937
+ return true;
13938
+ }
13939
+ }
13940
+ } else if (hasDriftRecursive(actualValue || {}, expectedValue)) {
13941
+ return true;
13942
+ }
13943
+ }
13944
+ return false;
13945
+ }
13946
+ function formatHierarchicalTree(preset, indent = " ") {
13947
+ const lines = [];
13948
+ for (const [key, value] of Object.entries(preset)) {
13949
+ if (!isObject2(value))
13950
+ continue;
13951
+ if (isLeafNode(value)) {
13952
+ const variant = value.variant ? ` (${value.variant})` : "";
13953
+ const otherProps = Object.keys(value).filter((k) => k !== "model" && k !== "variant").map((k) => `${k}: ${JSON.stringify(value[k])}`).join(", ");
13954
+ const extra = otherProps ? ` [${otherProps}]` : "";
13955
+ lines.push(`${indent}${key}: ${value.model}${variant}${extra}`);
13956
+ } else {
13957
+ lines.push(`${indent}${key}:`);
13958
+ lines.push(formatHierarchicalTree(value, `${indent} `));
13959
+ }
13960
+ }
13961
+ return lines.join(`
13962
+ `);
13963
+ }
13964
+
13914
13965
  class ModeManager {
13915
13966
  client;
13916
13967
  config = null;
@@ -13953,26 +14004,17 @@ class ModeManager {
13953
14004
  async hasConfigDrift(preset) {
13954
14005
  const opencodeConfig = await loadOpencodeConfig();
13955
14006
  const ohMyConfig = await loadOhMyOpencodeConfig();
13956
- if (preset.model && opencodeConfig) {
13957
- if (opencodeConfig.model !== preset.model) {
13958
- return true;
13959
- }
14007
+ if (!opencodeConfig && !ohMyConfig) {
14008
+ return false;
13960
14009
  }
13961
- if (opencodeConfig?.agent) {
13962
- for (const [agentName, agentPreset] of Object.entries(preset.opencode)) {
13963
- const actual = opencodeConfig.agent[agentName];
13964
- if (actual?.model !== agentPreset.model) {
13965
- return true;
13966
- }
13967
- }
14010
+ if (preset.model && opencodeConfig?.model !== preset.model) {
14011
+ return true;
13968
14012
  }
13969
- if (ohMyConfig?.agents) {
13970
- for (const [agentName, agentPreset] of Object.entries(preset["oh-my-opencode"])) {
13971
- const actual = ohMyConfig.agents[agentName];
13972
- if (actual?.model !== agentPreset.model) {
13973
- return true;
13974
- }
13975
- }
14013
+ if (opencodeConfig?.agent && hasDriftRecursive(opencodeConfig.agent, preset.opencode)) {
14014
+ return true;
14015
+ }
14016
+ if (ohMyConfig && hasDriftRecursive(ohMyConfig, preset["oh-my-opencode"])) {
14017
+ return true;
13976
14018
  }
13977
14019
  return false;
13978
14020
  }
@@ -14003,20 +14045,18 @@ ${modes}`;
14003
14045
  return `Current mode: ${currentMode} (preset not found)`;
14004
14046
  }
14005
14047
  const globalModel = preset.model ? `Global model: ${preset.model}` : "Global model: (not set)";
14006
- const opencodeAgents = Object.entries(preset.opencode).map(([name, cfg]) => ` - ${name}: ${cfg.model}`).join(`
14007
- `);
14008
- const ohMyOpencodeAgents = Object.entries(preset["oh-my-opencode"]).map(([name, cfg]) => ` - ${name}: ${cfg.model}`).join(`
14009
- `);
14048
+ const opencodeTree = formatHierarchicalTree(preset.opencode);
14049
+ const ohMyOpencodeTree = formatHierarchicalTree(preset["oh-my-opencode"]);
14010
14050
  return [
14011
14051
  `Current mode: ${currentMode}`,
14012
14052
  `Description: ${preset.description}`,
14013
14053
  globalModel,
14014
14054
  "",
14015
- "OpenCode agents:",
14016
- opencodeAgents || " (none configured)",
14055
+ "OpenCode config:",
14056
+ opencodeTree || " (none configured)",
14017
14057
  "",
14018
- "Oh-my-opencode agents:",
14019
- ohMyOpencodeAgents || " (none configured)"
14058
+ "Oh-my-opencode config:",
14059
+ ohMyOpencodeTree || " (none configured)"
14020
14060
  ].join(`
14021
14061
  `);
14022
14062
  }
@@ -14036,16 +14076,14 @@ ${modes}`;
14036
14076
  this.config = config2;
14037
14077
  await savePluginConfig(config2);
14038
14078
  results.push("agent-mode-switcher.json: updated");
14039
- try {
14040
- await this.client.tui.showToast({
14041
- body: {
14042
- title: "Mode Switched",
14043
- message: `Switched to "${modeName}". Restart opencode to apply.`,
14044
- variant: "warning",
14045
- duration: 5000
14046
- }
14047
- });
14048
- } catch {}
14079
+ this.client.tui.showToast({
14080
+ body: {
14081
+ title: "Mode Switched",
14082
+ message: `Switched to "${modeName}". Restart opencode to apply.`,
14083
+ variant: "warning",
14084
+ duration: 5000
14085
+ }
14086
+ }).catch(() => {});
14049
14087
  return [
14050
14088
  `Switched to ${modeName} mode`,
14051
14089
  preset.description,
@@ -14067,12 +14105,7 @@ ${modes}`;
14067
14105
  opencodeConfig.model = globalModel;
14068
14106
  }
14069
14107
  opencodeConfig.agent = opencodeConfig.agent || {};
14070
- for (const [agentName, preset] of Object.entries(agentPresets)) {
14071
- opencodeConfig.agent[agentName] = {
14072
- ...opencodeConfig.agent[agentName],
14073
- model: preset.model
14074
- };
14075
- }
14108
+ deepMergeModel(opencodeConfig.agent, agentPresets);
14076
14109
  await saveOpencodeConfig(opencodeConfig);
14077
14110
  return "updated";
14078
14111
  } catch (error45) {
@@ -14080,16 +14113,13 @@ ${modes}`;
14080
14113
  return `error: ${message}`;
14081
14114
  }
14082
14115
  }
14083
- async updateOhMyOpencodeConfig(agentPresets) {
14116
+ async updateOhMyOpencodeConfig(preset) {
14084
14117
  try {
14085
14118
  const ohMyConfig = await loadOhMyOpencodeConfig();
14086
14119
  if (!ohMyConfig) {
14087
14120
  return "skipped (not found)";
14088
14121
  }
14089
- ohMyConfig.agents = ohMyConfig.agents || {};
14090
- for (const [agentName, preset] of Object.entries(agentPresets)) {
14091
- ohMyConfig.agents[agentName] = { model: preset.model };
14092
- }
14122
+ deepMergeModel(ohMyConfig, preset);
14093
14123
  await saveOhMyOpencodeConfig(ohMyConfig);
14094
14124
  return "updated";
14095
14125
  } catch (error45) {
@@ -76,7 +76,7 @@ export declare class ModeManager {
76
76
  * Compares a mode preset against the actual opencode.json and
77
77
  * oh-my-opencode.json files to detect configuration drift.
78
78
  *
79
- * Checks global model and per-agent model values. Returns true
79
+ * Checks global model and per-agent model values recursively. Returns true
80
80
  * if any expected value differs from the actual file content.
81
81
  *
82
82
  * @param preset - The mode preset to compare against
@@ -134,8 +134,8 @@ export declare class ModeManager {
134
134
  * Returns a formatted multi-line string showing:
135
135
  * - Current mode name and description
136
136
  * - Global model setting (if configured)
137
- * - All OpenCode agents and their assigned models
138
- * - All oh-my-opencode agents and their assigned models
137
+ * - Hierarchical tree of OpenCode configuration
138
+ * - Hierarchical tree of oh-my-opencode configuration
139
139
  *
140
140
  * @returns Promise resolving to formatted status string
141
141
  * @example
@@ -147,9 +147,10 @@ export declare class ModeManager {
147
147
  * // Description: High-performance models for complex tasks
148
148
  * // Global model: (not set)
149
149
  * //
150
- * // OpenCode agents:
151
- * // - build: anthropic/claude-sonnet-4
152
- * // - plan: anthropic/claude-sonnet-4
150
+ * // OpenCode config:
151
+ * // agent:
152
+ * // build: anthropic/claude-sonnet-4
153
+ * // plan: anthropic/claude-sonnet-4
153
154
  * // ...
154
155
  * ```
155
156
  */
@@ -191,22 +192,23 @@ export declare class ModeManager {
191
192
  *
192
193
  * This internal method modifies the OpenCode configuration file to apply
193
194
  * the new preset's settings. It preserves other configuration properties
194
- * and only updates model-related fields.
195
+ * and only updates model-related fields using recursive merge.
195
196
  *
196
197
  * @param globalModel - Global model setting (optional). If provided, sets the top-level "model" field
197
- * @param agentPresets - Agent-specific model settings. Keys are agent names, values contain model strings
198
+ * @param agentPresets - Hierarchical preset structure for agent configuration
198
199
  * @returns Promise resolving to result status: "updated", "skipped (not found)", or "error: ..."
199
200
  * @private
200
201
  */
201
202
  private updateOpencodeConfig;
202
203
  /**
203
- * Updates oh-my-opencode.json agents section with preset values.
204
+ * Updates oh-my-opencode.json with preset values.
204
205
  *
205
206
  * This internal method modifies the oh-my-opencode configuration file
206
- * to apply the new preset's agent settings. Unlike opencode.json, this
207
- * only updates the agents section and doesn't set a global model.
207
+ * to apply the new preset's settings using recursive merge. The entire
208
+ * structure (agents, categories, etc.) is updated while preserving
209
+ * other properties.
208
210
  *
209
- * @param agentPresets - Agent-specific model settings. Keys are agent names, values contain model strings
211
+ * @param preset - Hierarchical preset structure for oh-my-opencode configuration
210
212
  * @returns Promise resolving to result status: "updated", "skipped (not found)", or "error: ..."
211
213
  * @private
212
214
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-agent-modes",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "OpenCode plugin to switch agent modes between performance and economy presets",
5
5
  "module": "src/index.ts",
6
6
  "main": "dist/index.js",