opencode-agent-modes 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -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
@@ -12332,6 +12332,10 @@ function tool(input) {
12332
12332
  tool.schema = exports_external;
12333
12333
  // src/config/types.ts
12334
12334
  var DEFAULT_ECONOMY_MODEL = "opencode/glm-4.7-free";
12335
+ // src/config/guards.ts
12336
+ function isObject2(obj) {
12337
+ return typeof obj === "object" && obj !== null && !Array.isArray(obj);
12338
+ }
12335
12339
  // src/config/loader.ts
12336
12340
  import { homedir } from "os";
12337
12341
  import { join } from "path";
@@ -13795,35 +13799,29 @@ async function pluginConfigExists() {
13795
13799
  return Bun.file(getPluginConfigPath()).exists();
13796
13800
  }
13797
13801
  // 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 };
13802
+ function applyEconomyModel(config2, economyModel) {
13803
+ const result = {};
13804
+ for (const [key, value] of Object.entries(config2)) {
13805
+ if (isObject2(value)) {
13806
+ if ("model" in value) {
13807
+ result[key] = {
13808
+ ...value,
13809
+ model: economyModel
13810
+ };
13811
+ } else {
13812
+ result[key] = applyEconomyModel(value, economyModel);
13824
13813
  }
13814
+ } else {
13815
+ result[key] = value;
13825
13816
  }
13826
13817
  }
13818
+ return result;
13819
+ }
13820
+ async function buildPerformancePreset() {
13821
+ const opencodeConfig = await loadOpencodeConfig();
13822
+ const ohMyOpencodeConfig = await loadOhMyOpencodeConfig();
13823
+ const opencodePreset = opencodeConfig?.agent || {};
13824
+ const ohMyOpencodePreset = ohMyOpencodeConfig || {};
13827
13825
  const globalModel = opencodeConfig?.model;
13828
13826
  return {
13829
13827
  description: "High-performance models for complex tasks",
@@ -13835,22 +13833,8 @@ async function buildPerformancePreset() {
13835
13833
  async function buildEconomyPreset() {
13836
13834
  const opencodeConfig = await loadOpencodeConfig();
13837
13835
  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
- }
13836
+ const opencodePreset = applyEconomyModel(opencodeConfig?.agent || {}, DEFAULT_ECONOMY_MODEL);
13837
+ const ohMyOpencodePreset = applyEconomyModel(ohMyOpencodeConfig || {}, DEFAULT_ECONOMY_MODEL);
13854
13838
  return {
13855
13839
  description: "Cost-efficient free model for routine tasks",
13856
13840
  model: DEFAULT_ECONOMY_MODEL,
@@ -13911,6 +13895,69 @@ function copyCommandFiles() {
13911
13895
  }
13912
13896
  }
13913
13897
  // src/modes/manager.ts
13898
+ function isLeafNode(value) {
13899
+ return "model" in value && typeof value.model === "string";
13900
+ }
13901
+ function deepMergeModel(target, preset) {
13902
+ for (const [key, value] of Object.entries(preset)) {
13903
+ if (!isObject2(value))
13904
+ continue;
13905
+ const actualValue = target[key];
13906
+ if (isLeafNode(value)) {
13907
+ const valueRecord = value;
13908
+ const existing = actualValue ?? {};
13909
+ const merged = {
13910
+ ...existing,
13911
+ ...valueRecord
13912
+ };
13913
+ target[key] = merged;
13914
+ } else {
13915
+ const childTarget = actualValue ?? {};
13916
+ target[key] = childTarget;
13917
+ deepMergeModel(childTarget, value);
13918
+ }
13919
+ }
13920
+ }
13921
+ function hasDriftRecursive(actual, expected) {
13922
+ for (const [key, expectedValue] of Object.entries(expected)) {
13923
+ if (!isObject2(expectedValue))
13924
+ continue;
13925
+ const actualValue = actual[key];
13926
+ if (isLeafNode(expectedValue)) {
13927
+ const actualObj = actualValue;
13928
+ if (!actualObj) {
13929
+ return true;
13930
+ }
13931
+ for (const [propKey, expectedPropValue] of Object.entries(expectedValue)) {
13932
+ if (actualObj[propKey] !== expectedPropValue) {
13933
+ return true;
13934
+ }
13935
+ }
13936
+ } else if (hasDriftRecursive(actualValue || {}, expectedValue)) {
13937
+ return true;
13938
+ }
13939
+ }
13940
+ return false;
13941
+ }
13942
+ function formatHierarchicalTree(preset, indent = " ") {
13943
+ const lines = [];
13944
+ for (const [key, value] of Object.entries(preset)) {
13945
+ if (!isObject2(value))
13946
+ continue;
13947
+ if (isLeafNode(value)) {
13948
+ const variant = value.variant ? ` (${value.variant})` : "";
13949
+ const otherProps = Object.keys(value).filter((k) => k !== "model" && k !== "variant").map((k) => `${k}: ${JSON.stringify(value[k])}`).join(", ");
13950
+ const extra = otherProps ? ` [${otherProps}]` : "";
13951
+ lines.push(`${indent}${key}: ${value.model}${variant}${extra}`);
13952
+ } else {
13953
+ lines.push(`${indent}${key}:`);
13954
+ lines.push(formatHierarchicalTree(value, `${indent} `));
13955
+ }
13956
+ }
13957
+ return lines.join(`
13958
+ `);
13959
+ }
13960
+
13914
13961
  class ModeManager {
13915
13962
  client;
13916
13963
  config = null;
@@ -13953,26 +14000,17 @@ class ModeManager {
13953
14000
  async hasConfigDrift(preset) {
13954
14001
  const opencodeConfig = await loadOpencodeConfig();
13955
14002
  const ohMyConfig = await loadOhMyOpencodeConfig();
13956
- if (preset.model && opencodeConfig) {
13957
- if (opencodeConfig.model !== preset.model) {
13958
- return true;
13959
- }
14003
+ if (!opencodeConfig && !ohMyConfig) {
14004
+ return false;
13960
14005
  }
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
- }
14006
+ if (preset.model && opencodeConfig?.model !== preset.model) {
14007
+ return true;
13968
14008
  }
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
- }
14009
+ if (opencodeConfig?.agent && hasDriftRecursive(opencodeConfig.agent, preset.opencode)) {
14010
+ return true;
14011
+ }
14012
+ if (ohMyConfig && hasDriftRecursive(ohMyConfig, preset["oh-my-opencode"])) {
14013
+ return true;
13976
14014
  }
13977
14015
  return false;
13978
14016
  }
@@ -14003,20 +14041,18 @@ ${modes}`;
14003
14041
  return `Current mode: ${currentMode} (preset not found)`;
14004
14042
  }
14005
14043
  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
- `);
14044
+ const opencodeTree = formatHierarchicalTree(preset.opencode);
14045
+ const ohMyOpencodeTree = formatHierarchicalTree(preset["oh-my-opencode"]);
14010
14046
  return [
14011
14047
  `Current mode: ${currentMode}`,
14012
14048
  `Description: ${preset.description}`,
14013
14049
  globalModel,
14014
14050
  "",
14015
- "OpenCode agents:",
14016
- opencodeAgents || " (none configured)",
14051
+ "OpenCode config:",
14052
+ opencodeTree || " (none configured)",
14017
14053
  "",
14018
- "Oh-my-opencode agents:",
14019
- ohMyOpencodeAgents || " (none configured)"
14054
+ "Oh-my-opencode config:",
14055
+ ohMyOpencodeTree || " (none configured)"
14020
14056
  ].join(`
14021
14057
  `);
14022
14058
  }
@@ -14036,16 +14072,14 @@ ${modes}`;
14036
14072
  this.config = config2;
14037
14073
  await savePluginConfig(config2);
14038
14074
  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 {}
14075
+ this.client.tui.showToast({
14076
+ body: {
14077
+ title: "Mode Switched",
14078
+ message: `Switched to "${modeName}". Restart opencode to apply.`,
14079
+ variant: "warning",
14080
+ duration: 5000
14081
+ }
14082
+ }).catch(() => {});
14049
14083
  return [
14050
14084
  `Switched to ${modeName} mode`,
14051
14085
  preset.description,
@@ -14067,12 +14101,7 @@ ${modes}`;
14067
14101
  opencodeConfig.model = globalModel;
14068
14102
  }
14069
14103
  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
- }
14104
+ deepMergeModel(opencodeConfig.agent, agentPresets);
14076
14105
  await saveOpencodeConfig(opencodeConfig);
14077
14106
  return "updated";
14078
14107
  } catch (error45) {
@@ -14080,16 +14109,13 @@ ${modes}`;
14080
14109
  return `error: ${message}`;
14081
14110
  }
14082
14111
  }
14083
- async updateOhMyOpencodeConfig(agentPresets) {
14112
+ async updateOhMyOpencodeConfig(preset) {
14084
14113
  try {
14085
14114
  const ohMyConfig = await loadOhMyOpencodeConfig();
14086
14115
  if (!ohMyConfig) {
14087
14116
  return "skipped (not found)";
14088
14117
  }
14089
- ohMyConfig.agents = ohMyConfig.agents || {};
14090
- for (const [agentName, preset] of Object.entries(agentPresets)) {
14091
- ohMyConfig.agents[agentName] = { model: preset.model };
14092
- }
14118
+ deepMergeModel(ohMyConfig, preset);
14093
14119
  await saveOhMyOpencodeConfig(ohMyConfig);
14094
14120
  return "updated";
14095
14121
  } 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.0",
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",