opencode-agent-modes 0.1.2 → 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
@@ -147,7 +147,42 @@ To add a custom preset (e.g., "premium"):
147
147
 
148
148
  3. Restart opencode to apply changes.
149
149
 
150
- ## Notes
150
+ > [!INFO]
151
+ > - Changes require an opencode restart to take effect
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.
151
157
 
152
- - Changes require an opencode restart to take effect
153
- - Custom mode presets can be added by editing the configuration file
158
+ ## Development
159
+
160
+ This project uses [Bun](https://bun.sh/) as the runtime and package manager.
161
+
162
+ ### Prerequisites
163
+
164
+ - [Bun](https://bun.sh/) v1.0 or later
165
+
166
+ ### Setup
167
+
168
+ ```bash
169
+ # Clone the repository
170
+ git clone https://github.com/j4rviscmd/opencode-agent-modes.git
171
+ cd opencode-agent-modes
172
+
173
+ # Install dependencies
174
+ bun install
175
+
176
+ # Run tests
177
+ bun test
178
+
179
+ # Type check
180
+ bun run typecheck
181
+
182
+ # Lint & format
183
+ bun run lint
184
+ bun run format
185
+
186
+ # Build
187
+ bun run build
188
+ ```
@@ -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,10 +1,46 @@
1
1
  import type { ModeSwitcherConfig } from './types.ts';
2
2
  /**
3
- * Initialize the plugin configuration if it doesn't exist
4
- * @returns The configuration (existing or newly created)
3
+ * Initializes the plugin configuration if it doesn't exist.
4
+ *
5
+ * This function performs the following steps:
6
+ * 1. Checks if a configuration file already exists
7
+ * 2. If exists, loads and returns it
8
+ * 3. If not, creates a new configuration by:
9
+ * - Building a performance preset from current settings
10
+ * - Building an economy preset with free models
11
+ * - Setting default mode to "performance"
12
+ * - Saving the configuration to disk
13
+ *
14
+ * This is called on plugin startup to ensure a valid configuration
15
+ * is always available.
16
+ *
17
+ * @returns Promise resolving to the configuration (existing or newly created)
18
+ * @throws {Error} If configuration creation or file I/O fails
19
+ * @example
20
+ * ```typescript
21
+ * const config = await initializeConfig();
22
+ * console.log(config.currentMode); // "performance"
23
+ * ```
5
24
  */
6
25
  export declare function initializeConfig(): Promise<ModeSwitcherConfig>;
7
26
  /**
8
- * Ensure configuration is valid and has required presets
27
+ * Validates that a configuration object is well-formed and has required presets.
28
+ *
29
+ * This function performs the following checks:
30
+ * - `currentMode` field is present and non-empty
31
+ * - `presets` object exists and contains at least one preset
32
+ * - A preset exists for the current mode
33
+ *
34
+ * @param config - The configuration object to validate
35
+ * @returns True if configuration is valid, false otherwise
36
+ * @example
37
+ * ```typescript
38
+ * const config = await loadPluginConfig();
39
+ * if (validateConfig(config)) {
40
+ * console.log('Configuration is valid');
41
+ * } else {
42
+ * console.error('Invalid configuration detected');
43
+ * }
44
+ * ```
9
45
  */
10
46
  export declare function validateConfig(config: ModeSwitcherConfig): boolean;
@@ -1,4 +1,4 @@
1
- import type { ModeSwitcherConfig, OpencodeConfig, OhMyOpencodeConfig } from './types.ts';
1
+ import type { ModeSwitcherConfig, OhMyOpencodeConfig, OpencodeConfig } from './types.ts';
2
2
  /**
3
3
  * Expands tilde (~) notation to the user's home directory path.
4
4
  *
@@ -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.d.ts CHANGED
@@ -1,9 +1,34 @@
1
- import type { Plugin } from "@opencode-ai/plugin";
2
1
  /**
3
- * OpenCode Agent Mode Switcher Plugin
2
+ * @fileoverview OpenCode Agent Mode Switcher Plugin.
4
3
  *
5
- * Allows switching between different agent mode presets (e.g., performance vs economy)
6
- * that configure which AI models are used for each agent type.
4
+ * This plugin provides tools for managing agent mode presets in OpenCode.
5
+ * It allows users to switch between different configurations (e.g., performance
6
+ * vs economy modes) that determine which AI models are used for each agent type.
7
+ *
8
+ * @module index
9
+ */
10
+ import type { Plugin } from '@opencode-ai/plugin';
11
+ /**
12
+ * OpenCode Agent Mode Switcher Plugin.
13
+ *
14
+ * Provides tools for switching between agent mode presets (e.g., performance
15
+ * vs economy) that configure which AI models are used for each agent type.
16
+ * The plugin initializes on startup by loading configurations and copying
17
+ * slash command files to the OpenCode command directory.
18
+ *
19
+ * @param params - Plugin initialization parameters
20
+ * @param params.client - OpenCode client for SDK interactions
21
+ * @returns Plugin object containing mode management tools
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * // Plugin is automatically loaded by OpenCode
26
+ * // Users can then use slash commands:
27
+ * // /mode-performance
28
+ * // /mode-economy
29
+ * // /mode-status
30
+ * // /mode-list
31
+ * ```
7
32
  */
8
33
  declare const modeSwitcherPlugin: Plugin;
9
34
  export default modeSwitcherPlugin;
package/dist/index.js CHANGED
@@ -12330,6 +12330,12 @@ function tool(input) {
12330
12330
  return input;
12331
12331
  }
12332
12332
  tool.schema = exports_external;
12333
+ // src/config/types.ts
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
+ }
12333
12339
  // src/config/loader.ts
12334
12340
  import { homedir } from "os";
12335
12341
  import { join } from "path";
@@ -13790,43 +13796,32 @@ async function saveOhMyOpencodeConfig(config2) {
13790
13796
  await saveJsonFile(getOhMyOpencodeConfigPath(), config2);
13791
13797
  }
13792
13798
  async function pluginConfigExists() {
13793
- const file2 = Bun.file(getPluginConfigPath());
13794
- return await file2.exists();
13799
+ return Bun.file(getPluginConfigPath()).exists();
13795
13800
  }
13796
-
13797
- // src/config/types.ts
13798
- var DEFAULT_ECONOMY_MODEL = "opencode/glm-4.7-free";
13799
-
13800
13801
  // src/config/initializer.ts
13801
- var OPENCODE_AGENTS = [
13802
- "build",
13803
- "plan",
13804
- "summary",
13805
- "compaction",
13806
- "title",
13807
- "explore",
13808
- "general"
13809
- ];
13810
- async function buildPerformancePreset() {
13811
- const opencodeConfig = await loadOpencodeConfig();
13812
- const ohMyOpencodeConfig = await loadOhMyOpencodeConfig();
13813
- const opencodePreset = {};
13814
- const ohMyOpencodePreset = {};
13815
- if (opencodeConfig?.agent) {
13816
- for (const agentName of OPENCODE_AGENTS) {
13817
- const agentConfig = opencodeConfig.agent[agentName];
13818
- if (agentConfig?.model) {
13819
- opencodePreset[agentName] = { model: agentConfig.model };
13820
- }
13821
- }
13822
- }
13823
- if (ohMyOpencodeConfig?.agents) {
13824
- for (const [agentName, agentConfig] of Object.entries(ohMyOpencodeConfig.agents)) {
13825
- if (agentConfig?.model) {
13826
- 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);
13827
13813
  }
13814
+ } else {
13815
+ result[key] = value;
13828
13816
  }
13829
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 || {};
13830
13825
  const globalModel = opencodeConfig?.model;
13831
13826
  return {
13832
13827
  description: "High-performance models for complex tasks",
@@ -13838,22 +13833,8 @@ async function buildPerformancePreset() {
13838
13833
  async function buildEconomyPreset() {
13839
13834
  const opencodeConfig = await loadOpencodeConfig();
13840
13835
  const ohMyOpencodeConfig = await loadOhMyOpencodeConfig();
13841
- const opencodePreset = {};
13842
- const ohMyOpencodePreset = {};
13843
- if (opencodeConfig?.agent) {
13844
- for (const agentName of Object.keys(opencodeConfig.agent)) {
13845
- opencodePreset[agentName] = { model: DEFAULT_ECONOMY_MODEL };
13846
- }
13847
- } else {
13848
- for (const agentName of OPENCODE_AGENTS) {
13849
- opencodePreset[agentName] = { model: DEFAULT_ECONOMY_MODEL };
13850
- }
13851
- }
13852
- if (ohMyOpencodeConfig?.agents) {
13853
- for (const agentName of Object.keys(ohMyOpencodeConfig.agents)) {
13854
- ohMyOpencodePreset[agentName] = { model: DEFAULT_ECONOMY_MODEL };
13855
- }
13856
- }
13836
+ const opencodePreset = applyEconomyModel(opencodeConfig?.agent || {}, DEFAULT_ECONOMY_MODEL);
13837
+ const ohMyOpencodePreset = applyEconomyModel(ohMyOpencodeConfig || {}, DEFAULT_ECONOMY_MODEL);
13857
13838
  return {
13858
13839
  description: "Cost-efficient free model for routine tasks",
13859
13840
  model: DEFAULT_ECONOMY_MODEL,
@@ -13882,8 +13863,101 @@ async function initializeConfig() {
13882
13863
  await savePluginConfig(config2);
13883
13864
  return config2;
13884
13865
  }
13885
-
13866
+ // src/config/command-installer.ts
13867
+ import { copyFileSync, existsSync, mkdirSync, readdirSync } from "fs";
13868
+ import { homedir as homedir2 } from "os";
13869
+ import { dirname, join as join2 } from "path";
13870
+ import { fileURLToPath } from "url";
13871
+ var COMMANDS_DEST = join2(homedir2(), ".config", "opencode", "command");
13872
+ function findCommandsDir() {
13873
+ const __dirname2 = dirname(fileURLToPath(import.meta.url));
13874
+ const candidates = [
13875
+ join2(__dirname2, "..", "commands"),
13876
+ join2(__dirname2, "..", "..", "commands")
13877
+ ];
13878
+ return candidates.find(existsSync) ?? null;
13879
+ }
13880
+ function copyCommandFiles() {
13881
+ const commandsSrc = findCommandsDir();
13882
+ if (!commandsSrc) {
13883
+ return -1;
13884
+ }
13885
+ try {
13886
+ mkdirSync(COMMANDS_DEST, { recursive: true });
13887
+ const files = readdirSync(commandsSrc).filter((f) => f.endsWith(".md"));
13888
+ for (const file2 of files) {
13889
+ copyFileSync(join2(commandsSrc, file2), join2(COMMANDS_DEST, file2));
13890
+ }
13891
+ return files.length;
13892
+ } catch (error45) {
13893
+ console.warn("[agent-mode-switcher] Warning: Could not copy command files:", error45 instanceof Error ? error45.message : String(error45));
13894
+ return -1;
13895
+ }
13896
+ }
13886
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
+
13887
13961
  class ModeManager {
13888
13962
  client;
13889
13963
  config = null;
@@ -13892,6 +13966,7 @@ class ModeManager {
13892
13966
  }
13893
13967
  async initialize() {
13894
13968
  this.config = await initializeConfig();
13969
+ await this.applyCurrentModeIfNeeded();
13895
13970
  }
13896
13971
  async ensureConfig() {
13897
13972
  if (!this.config) {
@@ -13899,6 +13974,46 @@ class ModeManager {
13899
13974
  }
13900
13975
  return this.config;
13901
13976
  }
13977
+ async applyCurrentModeIfNeeded() {
13978
+ if (!this.config) {
13979
+ return;
13980
+ }
13981
+ const preset = this.config.presets[this.config.currentMode];
13982
+ if (!preset) {
13983
+ return;
13984
+ }
13985
+ const drifted = await this.hasConfigDrift(preset);
13986
+ if (!drifted) {
13987
+ return;
13988
+ }
13989
+ await this.updateOpencodeConfig(preset.model, preset.opencode);
13990
+ await this.updateOhMyOpencodeConfig(preset["oh-my-opencode"]);
13991
+ this.client.tui.showToast({
13992
+ body: {
13993
+ title: "Mode Applied",
13994
+ message: `Applied "${this.config.currentMode}" mode. Restart opencode to take effect.`,
13995
+ variant: "warning",
13996
+ duration: 5000
13997
+ }
13998
+ }).catch(() => {});
13999
+ }
14000
+ async hasConfigDrift(preset) {
14001
+ const opencodeConfig = await loadOpencodeConfig();
14002
+ const ohMyConfig = await loadOhMyOpencodeConfig();
14003
+ if (!opencodeConfig && !ohMyConfig) {
14004
+ return false;
14005
+ }
14006
+ if (preset.model && opencodeConfig?.model !== preset.model) {
14007
+ return true;
14008
+ }
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;
14014
+ }
14015
+ return false;
14016
+ }
13902
14017
  async getCurrentMode() {
13903
14018
  const config2 = await this.ensureConfig();
13904
14019
  return config2.currentMode;
@@ -13926,20 +14041,18 @@ ${modes}`;
13926
14041
  return `Current mode: ${currentMode} (preset not found)`;
13927
14042
  }
13928
14043
  const globalModel = preset.model ? `Global model: ${preset.model}` : "Global model: (not set)";
13929
- const opencodeAgents = Object.entries(preset.opencode).map(([name, cfg]) => ` - ${name}: ${cfg.model}`).join(`
13930
- `);
13931
- const ohMyOpencodeAgents = Object.entries(preset["oh-my-opencode"]).map(([name, cfg]) => ` - ${name}: ${cfg.model}`).join(`
13932
- `);
14044
+ const opencodeTree = formatHierarchicalTree(preset.opencode);
14045
+ const ohMyOpencodeTree = formatHierarchicalTree(preset["oh-my-opencode"]);
13933
14046
  return [
13934
14047
  `Current mode: ${currentMode}`,
13935
14048
  `Description: ${preset.description}`,
13936
14049
  globalModel,
13937
14050
  "",
13938
- "OpenCode agents:",
13939
- opencodeAgents || " (none configured)",
14051
+ "OpenCode config:",
14052
+ opencodeTree || " (none configured)",
13940
14053
  "",
13941
- "Oh-my-opencode agents:",
13942
- ohMyOpencodeAgents || " (none configured)"
14054
+ "Oh-my-opencode config:",
14055
+ ohMyOpencodeTree || " (none configured)"
13943
14056
  ].join(`
13944
14057
  `);
13945
14058
  }
@@ -13959,16 +14072,14 @@ ${modes}`;
13959
14072
  this.config = config2;
13960
14073
  await savePluginConfig(config2);
13961
14074
  results.push("agent-mode-switcher.json: updated");
13962
- try {
13963
- await this.client.tui.showToast({
13964
- body: {
13965
- title: "Mode Switched",
13966
- message: `Switched to "${modeName}". Restart opencode to apply.`,
13967
- variant: "warning",
13968
- duration: 5000
13969
- }
13970
- });
13971
- } 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(() => {});
13972
14083
  return [
13973
14084
  `Switched to ${modeName} mode`,
13974
14085
  preset.description,
@@ -13989,15 +14100,8 @@ ${modes}`;
13989
14100
  if (globalModel) {
13990
14101
  opencodeConfig.model = globalModel;
13991
14102
  }
13992
- if (Object.keys(agentPresets).length > 0) {
13993
- opencodeConfig.agent = opencodeConfig.agent || {};
13994
- for (const [agentName, preset] of Object.entries(agentPresets)) {
13995
- opencodeConfig.agent[agentName] = {
13996
- ...opencodeConfig.agent[agentName],
13997
- model: preset.model
13998
- };
13999
- }
14000
- }
14103
+ opencodeConfig.agent = opencodeConfig.agent || {};
14104
+ deepMergeModel(opencodeConfig.agent, agentPresets);
14001
14105
  await saveOpencodeConfig(opencodeConfig);
14002
14106
  return "updated";
14003
14107
  } catch (error45) {
@@ -14005,16 +14109,13 @@ ${modes}`;
14005
14109
  return `error: ${message}`;
14006
14110
  }
14007
14111
  }
14008
- async updateOhMyOpencodeConfig(agentPresets) {
14112
+ async updateOhMyOpencodeConfig(preset) {
14009
14113
  try {
14010
14114
  const ohMyConfig = await loadOhMyOpencodeConfig();
14011
14115
  if (!ohMyConfig) {
14012
14116
  return "skipped (not found)";
14013
14117
  }
14014
- ohMyConfig.agents = ohMyConfig.agents || {};
14015
- for (const [agentName, preset] of Object.entries(agentPresets)) {
14016
- ohMyConfig.agents[agentName] = { model: preset.model };
14017
- }
14118
+ deepMergeModel(ohMyConfig, preset);
14018
14119
  await saveOhMyOpencodeConfig(ohMyConfig);
14019
14120
  return "updated";
14020
14121
  } catch (error45) {
@@ -14027,37 +14128,6 @@ ${modes}`;
14027
14128
  return config2.showToastOnStartup;
14028
14129
  }
14029
14130
  }
14030
- // src/config/command-installer.ts
14031
- import { copyFileSync, mkdirSync, readdirSync, existsSync } from "fs";
14032
- import { homedir as homedir2 } from "os";
14033
- import { dirname, join as join2 } from "path";
14034
- import { fileURLToPath } from "url";
14035
- var COMMANDS_DEST = join2(homedir2(), ".config", "opencode", "command");
14036
- function findCommandsDir() {
14037
- const __dirname2 = dirname(fileURLToPath(import.meta.url));
14038
- const candidates = [
14039
- join2(__dirname2, "..", "commands"),
14040
- join2(__dirname2, "..", "..", "commands")
14041
- ];
14042
- return candidates.find(existsSync) ?? null;
14043
- }
14044
- function copyCommandFiles() {
14045
- const commandsSrc = findCommandsDir();
14046
- if (!commandsSrc) {
14047
- return -1;
14048
- }
14049
- try {
14050
- mkdirSync(COMMANDS_DEST, { recursive: true });
14051
- const files = readdirSync(commandsSrc).filter((f) => f.endsWith(".md"));
14052
- for (const file2 of files) {
14053
- copyFileSync(join2(commandsSrc, file2), join2(COMMANDS_DEST, file2));
14054
- }
14055
- return files.length;
14056
- } catch (error45) {
14057
- console.warn("[agent-mode-switcher] Warning: Could not copy command files:", error45 instanceof Error ? error45.message : String(error45));
14058
- return -1;
14059
- }
14060
- }
14061
14131
  // src/index.ts
14062
14132
  var modeSwitcherPlugin = async ({ client }) => {
14063
14133
  const modeManager = new ModeManager(client);
@@ -1,54 +1,235 @@
1
1
  import type { OpencodeClient } from '@opencode-ai/sdk';
2
2
  import type { ModePreset } from '../config/types.ts';
3
3
  /**
4
- * Manages agent mode switching between different presets
4
+ * Manages agent mode switching between different presets.
5
+ *
6
+ * This class provides the core functionality for switching between
7
+ * performance and economy modes (or custom presets). It handles:
8
+ * - Loading and managing plugin configuration
9
+ * - Switching between different model presets
10
+ * - Updating OpenCode configuration files
11
+ * - Providing status and listing available modes
12
+ *
13
+ * The manager updates three configuration files when switching modes:
14
+ * - `~/.config/opencode/agent-mode-switcher.json` (plugin state)
15
+ * - `~/.config/opencode/opencode.json` (OpenCode agents)
16
+ * - `~/.config/opencode/oh-my-opencode.json` (oh-my-opencode agents)
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const manager = new ModeManager(client);
21
+ * await manager.initialize();
22
+ *
23
+ * // Switch to economy mode
24
+ * const result = await manager.switchMode('economy');
25
+ * console.log(result);
26
+ *
27
+ * // Get current status
28
+ * const status = await manager.getStatus();
29
+ * console.log(status);
30
+ * ```
5
31
  */
6
32
  export declare class ModeManager {
7
33
  private readonly client;
8
34
  private config;
9
35
  constructor(client: OpencodeClient);
10
36
  /**
11
- * Initialize the mode manager and load configuration
37
+ * Initializes the mode manager and loads configuration.
38
+ *
39
+ * This method should be called before using any other manager methods.
40
+ * It loads or creates the plugin configuration file, ensuring all
41
+ * required presets are available.
42
+ *
43
+ * @throws {Error} If configuration initialization fails
44
+ * @example
45
+ * ```typescript
46
+ * const manager = new ModeManager(client);
47
+ * await manager.initialize();
48
+ * ```
12
49
  */
13
50
  initialize(): Promise<void>;
14
51
  /**
15
- * Ensure configuration is loaded
52
+ * Ensures configuration is loaded before any operation.
53
+ *
54
+ * This internal method is called by all public methods to lazily
55
+ * initialize the configuration if it hasn't been loaded yet.
56
+ *
57
+ * @returns Promise resolving to the loaded configuration
58
+ * @throws {Error} If configuration loading fails
59
+ * @private
16
60
  */
17
61
  private ensureConfig;
18
62
  /**
19
- * Get the current mode name
63
+ * Checks if actual config files have drifted from the current
64
+ * mode preset and applies the preset if needed.
65
+ *
66
+ * This handles the case where a user manually edits
67
+ * `agent-mode-switcher.json` to change `currentMode` while
68
+ * OpenCode is not running. On next startup, the actual config
69
+ * files are updated to match the expected preset values,
70
+ * and a toast notification prompts the user to restart.
71
+ *
72
+ * @private
73
+ */
74
+ private applyCurrentModeIfNeeded;
75
+ /**
76
+ * Compares a mode preset against the actual opencode.json and
77
+ * oh-my-opencode.json files to detect configuration drift.
78
+ *
79
+ * Checks global model and per-agent model values recursively. Returns true
80
+ * if any expected value differs from the actual file content.
81
+ *
82
+ * @param preset - The mode preset to compare against
83
+ * @returns True if actual configs differ from the preset
84
+ * @private
85
+ */
86
+ private hasConfigDrift;
87
+ /**
88
+ * Gets the name of the currently active mode.
89
+ *
90
+ * @returns Promise resolving to the current mode name (e.g., "performance", "economy")
91
+ * @example
92
+ * ```typescript
93
+ * const currentMode = await manager.getCurrentMode();
94
+ * console.log(`Current mode: ${currentMode}`);
95
+ * ```
20
96
  */
21
97
  getCurrentMode(): Promise<string>;
22
98
  /**
23
- * Get a specific preset by name
99
+ * Gets a specific mode preset by name.
100
+ *
101
+ * @param modeName - The name of the mode to retrieve (e.g., "performance", "economy")
102
+ * @returns Promise resolving to the preset configuration, or undefined if not found
103
+ * @example
104
+ * ```typescript
105
+ * const preset = await manager.getPreset('economy');
106
+ * if (preset) {
107
+ * console.log(preset.description);
108
+ * console.log(preset.model);
109
+ * }
110
+ * ```
24
111
  */
25
112
  getPreset(modeName: string): Promise<ModePreset | undefined>;
26
113
  /**
27
- * Get all available mode names
114
+ * Gets a formatted list of all available modes.
115
+ *
116
+ * Returns a multi-line string listing each mode with its description,
117
+ * marking the currently active mode with "(current)".
118
+ *
119
+ * @returns Promise resolving to formatted string listing all available modes
120
+ * @example
121
+ * ```typescript
122
+ * const list = await manager.listModes();
123
+ * console.log(list);
124
+ * // Output:
125
+ * // Available modes:
126
+ * // - performance (current): High-performance models for complex tasks
127
+ * // - economy: Cost-efficient free model for routine tasks
128
+ * ```
28
129
  */
29
130
  listModes(): Promise<string>;
30
131
  /**
31
- * Get current status including mode and agent configurations
132
+ * Gets detailed status information for the current mode.
133
+ *
134
+ * Returns a formatted multi-line string showing:
135
+ * - Current mode name and description
136
+ * - Global model setting (if configured)
137
+ * - Hierarchical tree of OpenCode configuration
138
+ * - Hierarchical tree of oh-my-opencode configuration
139
+ *
140
+ * @returns Promise resolving to formatted status string
141
+ * @example
142
+ * ```typescript
143
+ * const status = await manager.getStatus();
144
+ * console.log(status);
145
+ * // Output:
146
+ * // Current mode: performance
147
+ * // Description: High-performance models for complex tasks
148
+ * // Global model: (not set)
149
+ * //
150
+ * // OpenCode config:
151
+ * // agent:
152
+ * // build: anthropic/claude-sonnet-4
153
+ * // plan: anthropic/claude-sonnet-4
154
+ * // ...
155
+ * ```
32
156
  */
33
157
  getStatus(): Promise<string>;
34
158
  /**
35
- * Switch to a different mode
159
+ * Switches to a different mode by updating all configuration files.
160
+ *
161
+ * This method performs the following operations:
162
+ * 1. Validates that the requested mode exists
163
+ * 2. Updates `opencode.json` with new global model and agent settings
164
+ * 3. Updates `oh-my-opencode.json` with new agent settings
165
+ * 4. Updates `agent-mode-switcher.json` with the new current mode
166
+ * 5. Shows a toast notification (if available)
167
+ *
168
+ * Configuration files that don't exist are skipped with a warning.
169
+ * Changes take effect after restarting OpenCode.
170
+ *
171
+ * @param modeName - The name of the mode to switch to
172
+ * @returns Promise resolving to a formatted result message with status of each config update
173
+ * @example
174
+ * ```typescript
175
+ * const result = await manager.switchMode('economy');
176
+ * console.log(result);
177
+ * // Output:
178
+ * // Switched to economy mode
179
+ * // Cost-efficient free model for routine tasks
180
+ * //
181
+ * // Results:
182
+ * // - opencode.json: updated
183
+ * // - oh-my-opencode.json: updated
184
+ * // - agent-mode-switcher.json: updated
185
+ * //
186
+ * // Note: Restart opencode to apply changes.
187
+ * ```
36
188
  */
37
189
  switchMode(modeName: string): Promise<string>;
38
190
  /**
39
- * Update opencode.json with global model and agent section
40
- * @param globalModel - Global model setting (optional)
41
- * @param agentPresets - Agent-specific model settings
42
- * @returns Result status: "updated", "skipped (not found)", or "error: ..."
191
+ * Updates opencode.json with global model and agent section.
192
+ *
193
+ * This internal method modifies the OpenCode configuration file to apply
194
+ * the new preset's settings. It preserves other configuration properties
195
+ * and only updates model-related fields using recursive merge.
196
+ *
197
+ * @param globalModel - Global model setting (optional). If provided, sets the top-level "model" field
198
+ * @param agentPresets - Hierarchical preset structure for agent configuration
199
+ * @returns Promise resolving to result status: "updated", "skipped (not found)", or "error: ..."
200
+ * @private
43
201
  */
44
202
  private updateOpencodeConfig;
45
203
  /**
46
- * Update oh-my-opencode.json agents section with preset values
47
- * @returns Result status: "updated", "skipped (not found)", or "error: ..."
204
+ * Updates oh-my-opencode.json with preset values.
205
+ *
206
+ * This internal method modifies the oh-my-opencode configuration file
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.
210
+ *
211
+ * @param preset - Hierarchical preset structure for oh-my-opencode configuration
212
+ * @returns Promise resolving to result status: "updated", "skipped (not found)", or "error: ..."
213
+ * @private
48
214
  */
49
215
  private updateOhMyOpencodeConfig;
50
216
  /**
51
- * Check if toast should be shown on startup
217
+ * Checks if a toast notification should be shown on plugin startup.
218
+ *
219
+ * This is controlled by the `showToastOnStartup` configuration flag,
220
+ * which can be useful for reminding users of the current mode when
221
+ * OpenCode starts.
222
+ *
223
+ * @returns Promise resolving to true if toast should be shown, false otherwise
224
+ * @example
225
+ * ```typescript
226
+ * if (await manager.shouldShowToastOnStartup()) {
227
+ * const mode = await manager.getCurrentMode();
228
+ * await client.tui.showToast({
229
+ * body: { message: `Current mode: ${mode}` }
230
+ * });
231
+ * }
232
+ * ```
52
233
  */
53
234
  shouldShowToastOnStartup(): Promise<boolean>;
54
235
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-agent-modes",
3
- "version": "0.1.2",
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",
@@ -25,29 +25,23 @@
25
25
  "scripts": {
26
26
  "typecheck": "tsc --noEmit",
27
27
  "build": "bun build src/index.ts --outdir dist --format esm --target bun && tsc -p tsconfig.build.json",
28
- "lint": "bunx biome check src/",
29
- "format": "prettier --write 'src/**/*.ts'",
30
- "format:check": "prettier --check 'src/**/*.ts'",
28
+ "lint": "bunx biome check .",
29
+ "format": "bunx biome format --write .",
30
+ "format:check": "bunx biome format .",
31
31
  "test": "bun test",
32
32
  "test:watch": "bun test --watch",
33
33
  "test:coverage": "bun test --coverage",
34
34
  "prepublishOnly": "bun run build"
35
35
  },
36
- "keywords": [
37
- "opencode",
38
- "plugin",
39
- "agent",
40
- "mode",
41
- "switcher"
42
- ],
36
+ "keywords": ["opencode", "plugin", "agent", "mode", "switcher"],
43
37
  "author": "j4rviscmd",
44
38
  "license": "MIT",
45
39
  "engines": {
46
40
  "node": ">=18"
47
41
  },
48
42
  "devDependencies": {
49
- "@types/bun": "latest",
50
- "prettier": "^3.8.0"
43
+ "@biomejs/biome": "^1.9.4",
44
+ "@types/bun": "latest"
51
45
  },
52
46
  "peerDependencies": {
53
47
  "typescript": "^5"