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 +38 -3
- package/dist/config/guards.d.ts +14 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/config/initializer.d.ts +39 -3
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/types.d.ts +36 -8
- package/dist/index.d.ts +29 -4
- package/dist/index.js +182 -112
- package/dist/modes/manager.d.ts +196 -15
- package/package.json +7 -13
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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>;
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,10 +1,46 @@
|
|
|
1
1
|
import type { ModeSwitcherConfig } from './types.ts';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
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;
|
package/dist/config/loader.d.ts
CHANGED
package/dist/config/types.d.ts
CHANGED
|
@@ -1,17 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
5
|
-
model
|
|
7
|
+
export interface ModelConfig {
|
|
8
|
+
model?: string;
|
|
9
|
+
variant?: string;
|
|
10
|
+
[key: string]: unknown;
|
|
6
11
|
}
|
|
7
12
|
/**
|
|
8
|
-
*
|
|
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:
|
|
14
|
-
'oh-my-opencode':
|
|
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?:
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
13802
|
-
|
|
13803
|
-
|
|
13804
|
-
|
|
13805
|
-
|
|
13806
|
-
|
|
13807
|
-
|
|
13808
|
-
|
|
13809
|
-
|
|
13810
|
-
|
|
13811
|
-
|
|
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
|
|
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
|
|
13939
|
-
|
|
14051
|
+
"OpenCode config:",
|
|
14052
|
+
opencodeTree || " (none configured)",
|
|
13940
14053
|
"",
|
|
13941
|
-
"Oh-my-opencode
|
|
13942
|
-
|
|
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
|
-
|
|
13963
|
-
|
|
13964
|
-
|
|
13965
|
-
|
|
13966
|
-
|
|
13967
|
-
|
|
13968
|
-
|
|
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
|
-
|
|
13993
|
-
|
|
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(
|
|
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
|
|
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);
|
package/dist/modes/manager.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
-
*
|
|
47
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
29
|
-
"format": "
|
|
30
|
-
"format:check": "
|
|
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
|
-
"@
|
|
50
|
-
"
|
|
43
|
+
"@biomejs/biome": "^1.9.4",
|
|
44
|
+
"@types/bun": "latest"
|
|
51
45
|
},
|
|
52
46
|
"peerDependencies": {
|
|
53
47
|
"typescript": "^5"
|