opencode-morphllm 0.0.7 → 0.0.9
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 +1 -0
- package/dist/morph/router.js +1 -1
- package/package.json +2 -1
- package/.gitleaks.toml +0 -6
- package/.husky/pre-commit +0 -6
- package/.prettierignore +0 -1
- package/.prettierrc +0 -8
- package/bun.lock +0 -364
- package/bunfig.toml +0 -4
- package/dist/morph/mcps.test.d.ts +0 -1
- package/dist/morph/mcps.test.js +0 -39
- package/dist/morph/router.test.d.ts +0 -1
- package/dist/morph/router.test.js +0 -239
- package/dist/shared/config.test.d.ts +0 -1
- package/dist/shared/config.test.js +0 -201
- package/dist/shared/opencode-config-dir.test.d.ts +0 -1
- package/dist/shared/opencode-config-dir.test.js +0 -310
- package/src/index.ts +0 -22
- package/src/morph/mcps.test.ts +0 -51
- package/src/morph/mcps.ts +0 -18
- package/src/morph/router.test.ts +0 -267
- package/src/morph/router.ts +0 -111
- package/src/shared/config.test.ts +0 -257
- package/src/shared/config.ts +0 -118
- package/src/shared/opencode-config-dir.test.ts +0 -404
- package/src/shared/opencode-config-dir.ts +0 -156
- package/tsconfig.json +0 -20
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
4
|
-
import { getOpenCodeConfigDir, getOpenCodeConfigPaths, isDevBuild, detectExistingConfigDir, TAURI_APP_IDENTIFIER, TAURI_APP_IDENTIFIER_DEV, } from './opencode-config-dir';
|
|
5
|
-
describe('opencode-config-dir', () => {
|
|
6
|
-
let originalPlatform;
|
|
7
|
-
let originalEnv;
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
originalPlatform = process.platform;
|
|
10
|
-
originalEnv = {
|
|
11
|
-
APPDATA: process.env.APPDATA,
|
|
12
|
-
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
|
|
13
|
-
XDG_DATA_HOME: process.env.XDG_DATA_HOME,
|
|
14
|
-
OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR,
|
|
15
|
-
};
|
|
16
|
-
});
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
19
|
-
for (const [key, value] of Object.entries(originalEnv)) {
|
|
20
|
-
if (value !== undefined) {
|
|
21
|
-
process.env[key] = value;
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
delete process.env[key];
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
describe('OPENCODE_CONFIG_DIR environment variable', () => {
|
|
29
|
-
test('returns OPENCODE_CONFIG_DIR when env var is set', () => {
|
|
30
|
-
// #given OPENCODE_CONFIG_DIR is set to a custom path
|
|
31
|
-
process.env.OPENCODE_CONFIG_DIR = '/custom/opencode/path';
|
|
32
|
-
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
33
|
-
// #when getOpenCodeConfigDir is called with binary="opencode"
|
|
34
|
-
const result = getOpenCodeConfigDir({
|
|
35
|
-
binary: 'opencode',
|
|
36
|
-
version: '1.0.200',
|
|
37
|
-
});
|
|
38
|
-
// #then returns the custom path
|
|
39
|
-
expect(result).toBe('/custom/opencode/path');
|
|
40
|
-
});
|
|
41
|
-
test('falls back to default when env var is not set', () => {
|
|
42
|
-
// #given OPENCODE_CONFIG_DIR is not set, platform is Linux
|
|
43
|
-
delete process.env.OPENCODE_CONFIG_DIR;
|
|
44
|
-
delete process.env.XDG_CONFIG_HOME;
|
|
45
|
-
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
46
|
-
// #when getOpenCodeConfigDir is called with binary="opencode"
|
|
47
|
-
const result = getOpenCodeConfigDir({
|
|
48
|
-
binary: 'opencode',
|
|
49
|
-
version: '1.0.200',
|
|
50
|
-
});
|
|
51
|
-
// #then returns default ~/.config/opencode
|
|
52
|
-
expect(result).toBe(join(homedir(), '.config', 'opencode'));
|
|
53
|
-
});
|
|
54
|
-
test('falls back to default when env var is empty string', () => {
|
|
55
|
-
// #given OPENCODE_CONFIG_DIR is set to empty string
|
|
56
|
-
process.env.OPENCODE_CONFIG_DIR = '';
|
|
57
|
-
delete process.env.XDG_CONFIG_HOME;
|
|
58
|
-
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
59
|
-
// #when getOpenCodeConfigDir is called with binary="opencode"
|
|
60
|
-
const result = getOpenCodeConfigDir({
|
|
61
|
-
binary: 'opencode',
|
|
62
|
-
version: '1.0.200',
|
|
63
|
-
});
|
|
64
|
-
// #then returns default ~/.config/opencode
|
|
65
|
-
expect(result).toBe(join(homedir(), '.config', 'opencode'));
|
|
66
|
-
});
|
|
67
|
-
test('falls back to default when env var is whitespace only', () => {
|
|
68
|
-
// #given OPENCODE_CONFIG_DIR is set to whitespace only
|
|
69
|
-
process.env.OPENCODE_CONFIG_DIR = ' ';
|
|
70
|
-
delete process.env.XDG_CONFIG_HOME;
|
|
71
|
-
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
72
|
-
// #when getOpenCodeConfigDir is called with binary="opencode"
|
|
73
|
-
const result = getOpenCodeConfigDir({
|
|
74
|
-
binary: 'opencode',
|
|
75
|
-
version: '1.0.200',
|
|
76
|
-
});
|
|
77
|
-
// #then returns default ~/.config/opencode
|
|
78
|
-
expect(result).toBe(join(homedir(), '.config', 'opencode'));
|
|
79
|
-
});
|
|
80
|
-
test('resolves relative path to absolute path', () => {
|
|
81
|
-
// #given OPENCODE_CONFIG_DIR is set to a relative path
|
|
82
|
-
process.env.OPENCODE_CONFIG_DIR = './my-opencode-config';
|
|
83
|
-
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
84
|
-
// #when getOpenCodeConfigDir is called with binary="opencode"
|
|
85
|
-
const result = getOpenCodeConfigDir({
|
|
86
|
-
binary: 'opencode',
|
|
87
|
-
version: '1.0.200',
|
|
88
|
-
});
|
|
89
|
-
// #then returns resolved absolute path
|
|
90
|
-
expect(result).toBe(resolve('./my-opencode-config'));
|
|
91
|
-
});
|
|
92
|
-
test('OPENCODE_CONFIG_DIR takes priority over XDG_CONFIG_HOME', () => {
|
|
93
|
-
// #given both OPENCODE_CONFIG_DIR and XDG_CONFIG_HOME are set
|
|
94
|
-
process.env.OPENCODE_CONFIG_DIR = '/custom/opencode/path';
|
|
95
|
-
process.env.XDG_CONFIG_HOME = '/xdg/config';
|
|
96
|
-
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
97
|
-
// #when getOpenCodeConfigDir is called with binary="opencode"
|
|
98
|
-
const result = getOpenCodeConfigDir({
|
|
99
|
-
binary: 'opencode',
|
|
100
|
-
version: '1.0.200',
|
|
101
|
-
});
|
|
102
|
-
// #then OPENCODE_CONFIG_DIR takes priority
|
|
103
|
-
expect(result).toBe('/custom/opencode/path');
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
describe('isDevBuild', () => {
|
|
107
|
-
test('returns false for null version', () => {
|
|
108
|
-
expect(isDevBuild(null)).toBe(false);
|
|
109
|
-
});
|
|
110
|
-
test('returns false for undefined version', () => {
|
|
111
|
-
expect(isDevBuild(undefined)).toBe(false);
|
|
112
|
-
});
|
|
113
|
-
test('returns false for production version', () => {
|
|
114
|
-
expect(isDevBuild('1.0.200')).toBe(false);
|
|
115
|
-
expect(isDevBuild('2.1.0')).toBe(false);
|
|
116
|
-
});
|
|
117
|
-
test('returns true for version containing -dev', () => {
|
|
118
|
-
expect(isDevBuild('1.0.0-dev')).toBe(true);
|
|
119
|
-
expect(isDevBuild('1.0.0-dev.123')).toBe(true);
|
|
120
|
-
});
|
|
121
|
-
test('returns true for version containing .dev', () => {
|
|
122
|
-
expect(isDevBuild('1.0.0.dev')).toBe(true);
|
|
123
|
-
expect(isDevBuild('1.0.0.dev.456')).toBe(true);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
describe('getOpenCodeConfigDir', () => {
|
|
127
|
-
describe('for opencode CLI binary', () => {
|
|
128
|
-
test('returns ~/.config/opencode on Linux', () => {
|
|
129
|
-
// #given opencode CLI binary detected, platform is Linux
|
|
130
|
-
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
131
|
-
delete process.env.XDG_CONFIG_HOME;
|
|
132
|
-
delete process.env.OPENCODE_CONFIG_DIR;
|
|
133
|
-
// #when getOpenCodeConfigDir is called with binary="opencode"
|
|
134
|
-
const result = getOpenCodeConfigDir({
|
|
135
|
-
binary: 'opencode',
|
|
136
|
-
version: '1.0.200',
|
|
137
|
-
});
|
|
138
|
-
// #then returns ~/.config/opencode
|
|
139
|
-
expect(result).toBe(join(homedir(), '.config', 'opencode'));
|
|
140
|
-
});
|
|
141
|
-
test('returns $XDG_CONFIG_HOME/opencode on Linux when XDG_CONFIG_HOME is set', () => {
|
|
142
|
-
// #given opencode CLI binary detected, platform is Linux with XDG_CONFIG_HOME set
|
|
143
|
-
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
144
|
-
process.env.XDG_CONFIG_HOME = '/custom/config';
|
|
145
|
-
delete process.env.OPENCODE_CONFIG_DIR;
|
|
146
|
-
// #when getOpenCodeConfigDir is called with binary="opencode"
|
|
147
|
-
const result = getOpenCodeConfigDir({
|
|
148
|
-
binary: 'opencode',
|
|
149
|
-
version: '1.0.200',
|
|
150
|
-
});
|
|
151
|
-
// #then returns $XDG_CONFIG_HOME/opencode
|
|
152
|
-
expect(result).toBe('/custom/config/opencode');
|
|
153
|
-
});
|
|
154
|
-
test('returns ~/.config/opencode on macOS', () => {
|
|
155
|
-
// #given opencode CLI binary detected, platform is macOS
|
|
156
|
-
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
157
|
-
delete process.env.XDG_CONFIG_HOME;
|
|
158
|
-
delete process.env.OPENCODE_CONFIG_DIR;
|
|
159
|
-
// #when getOpenCodeConfigDir is called with binary="opencode"
|
|
160
|
-
const result = getOpenCodeConfigDir({
|
|
161
|
-
binary: 'opencode',
|
|
162
|
-
version: '1.0.200',
|
|
163
|
-
});
|
|
164
|
-
// #then returns ~/.config/opencode
|
|
165
|
-
expect(result).toBe(join(homedir(), '.config', 'opencode'));
|
|
166
|
-
});
|
|
167
|
-
test('returns ~/.config/opencode on Windows by default', () => {
|
|
168
|
-
// #given opencode CLI binary detected, platform is Windows
|
|
169
|
-
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
170
|
-
delete process.env.APPDATA;
|
|
171
|
-
delete process.env.OPENCODE_CONFIG_DIR;
|
|
172
|
-
// #when getOpenCodeConfigDir is called with binary="opencode"
|
|
173
|
-
const result = getOpenCodeConfigDir({
|
|
174
|
-
binary: 'opencode',
|
|
175
|
-
version: '1.0.200',
|
|
176
|
-
checkExisting: false,
|
|
177
|
-
});
|
|
178
|
-
// #then returns ~/.config/opencode (cross-platform default)
|
|
179
|
-
expect(result).toBe(join(homedir(), '.config', 'opencode'));
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
describe('for opencode-desktop Tauri binary', () => {
|
|
183
|
-
test('returns ~/.config/ai.opencode.desktop on Linux', () => {
|
|
184
|
-
// #given opencode-desktop binary detected, platform is Linux
|
|
185
|
-
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
186
|
-
delete process.env.XDG_CONFIG_HOME;
|
|
187
|
-
// #when getOpenCodeConfigDir is called with binary="opencode-desktop"
|
|
188
|
-
const result = getOpenCodeConfigDir({
|
|
189
|
-
binary: 'opencode-desktop',
|
|
190
|
-
version: '1.0.200',
|
|
191
|
-
checkExisting: false,
|
|
192
|
-
});
|
|
193
|
-
// #then returns ~/.config/ai.opencode.desktop
|
|
194
|
-
expect(result).toBe(join(homedir(), '.config', TAURI_APP_IDENTIFIER));
|
|
195
|
-
});
|
|
196
|
-
test('returns ~/Library/Application Support/ai.opencode.desktop on macOS', () => {
|
|
197
|
-
// #given opencode-desktop binary detected, platform is macOS
|
|
198
|
-
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
199
|
-
// #when getOpenCodeConfigDir is called with binary="opencode-desktop"
|
|
200
|
-
const result = getOpenCodeConfigDir({
|
|
201
|
-
binary: 'opencode-desktop',
|
|
202
|
-
version: '1.0.200',
|
|
203
|
-
checkExisting: false,
|
|
204
|
-
});
|
|
205
|
-
// #then returns ~/Library/Application Support/ai.opencode.desktop
|
|
206
|
-
expect(result).toBe(join(homedir(), 'Library', 'Application Support', TAURI_APP_IDENTIFIER));
|
|
207
|
-
});
|
|
208
|
-
test('returns %APPDATA%/ai.opencode.desktop on Windows', () => {
|
|
209
|
-
// #given opencode-desktop binary detected, platform is Windows
|
|
210
|
-
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
211
|
-
process.env.APPDATA = 'C:\\Users\\TestUser\\AppData\\Roaming';
|
|
212
|
-
// #when getOpenCodeConfigDir is called with binary="opencode-desktop"
|
|
213
|
-
const result = getOpenCodeConfigDir({
|
|
214
|
-
binary: 'opencode-desktop',
|
|
215
|
-
version: '1.0.200',
|
|
216
|
-
checkExisting: false,
|
|
217
|
-
});
|
|
218
|
-
// #then returns %APPDATA%/ai.opencode.desktop
|
|
219
|
-
expect(result).toBe(join('C:\\Users\\TestUser\\AppData\\Roaming', TAURI_APP_IDENTIFIER));
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
describe('dev build detection', () => {
|
|
223
|
-
test('returns ai.opencode.desktop.dev path when dev version detected', () => {
|
|
224
|
-
// #given opencode-desktop dev version
|
|
225
|
-
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
226
|
-
delete process.env.XDG_CONFIG_HOME;
|
|
227
|
-
// #when getOpenCodeConfigDir is called with dev version
|
|
228
|
-
const result = getOpenCodeConfigDir({
|
|
229
|
-
binary: 'opencode-desktop',
|
|
230
|
-
version: '1.0.0-dev.123',
|
|
231
|
-
checkExisting: false,
|
|
232
|
-
});
|
|
233
|
-
// #then returns path with ai.opencode.desktop.dev
|
|
234
|
-
expect(result).toBe(join(homedir(), '.config', TAURI_APP_IDENTIFIER_DEV));
|
|
235
|
-
});
|
|
236
|
-
test('returns ai.opencode.desktop.dev on macOS for dev build', () => {
|
|
237
|
-
// #given opencode-desktop dev version on macOS
|
|
238
|
-
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
239
|
-
// #when getOpenCodeConfigDir is called with dev version
|
|
240
|
-
const result = getOpenCodeConfigDir({
|
|
241
|
-
binary: 'opencode-desktop',
|
|
242
|
-
version: '1.0.0-dev',
|
|
243
|
-
checkExisting: false,
|
|
244
|
-
});
|
|
245
|
-
// #then returns path with ai.opencode.desktop.dev
|
|
246
|
-
expect(result).toBe(join(homedir(), 'Library', 'Application Support', TAURI_APP_IDENTIFIER_DEV));
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
describe('getOpenCodeConfigPaths', () => {
|
|
251
|
-
test('returns all config paths for CLI binary', () => {
|
|
252
|
-
// #given opencode CLI binary on Linux
|
|
253
|
-
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
254
|
-
delete process.env.XDG_CONFIG_HOME;
|
|
255
|
-
delete process.env.OPENCODE_CONFIG_DIR;
|
|
256
|
-
// #when getOpenCodeConfigPaths is called
|
|
257
|
-
const paths = getOpenCodeConfigPaths({
|
|
258
|
-
binary: 'opencode',
|
|
259
|
-
version: '1.0.200',
|
|
260
|
-
});
|
|
261
|
-
// #then returns all expected paths
|
|
262
|
-
const expectedDir = join(homedir(), '.config', 'opencode');
|
|
263
|
-
expect(paths.configDir).toBe(expectedDir);
|
|
264
|
-
expect(paths.configJson).toBe(join(expectedDir, 'opencode.json'));
|
|
265
|
-
expect(paths.configJsonc).toBe(join(expectedDir, 'opencode.jsonc'));
|
|
266
|
-
expect(paths.packageJson).toBe(join(expectedDir, 'package.json'));
|
|
267
|
-
expect(paths.omoConfig).toBe(join(expectedDir, 'oh-my-opencode.json'));
|
|
268
|
-
});
|
|
269
|
-
test('returns all config paths for desktop binary', () => {
|
|
270
|
-
// #given opencode-desktop binary on macOS
|
|
271
|
-
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
272
|
-
// #when getOpenCodeConfigPaths is called
|
|
273
|
-
const paths = getOpenCodeConfigPaths({
|
|
274
|
-
binary: 'opencode-desktop',
|
|
275
|
-
version: '1.0.200',
|
|
276
|
-
checkExisting: false,
|
|
277
|
-
});
|
|
278
|
-
// #then returns all expected paths
|
|
279
|
-
const expectedDir = join(homedir(), 'Library', 'Application Support', TAURI_APP_IDENTIFIER);
|
|
280
|
-
expect(paths.configDir).toBe(expectedDir);
|
|
281
|
-
expect(paths.configJson).toBe(join(expectedDir, 'opencode.json'));
|
|
282
|
-
expect(paths.configJsonc).toBe(join(expectedDir, 'opencode.jsonc'));
|
|
283
|
-
expect(paths.packageJson).toBe(join(expectedDir, 'package.json'));
|
|
284
|
-
expect(paths.omoConfig).toBe(join(expectedDir, 'oh-my-opencode.json'));
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
describe('detectExistingConfigDir', () => {
|
|
288
|
-
test('returns null when no config exists', () => {
|
|
289
|
-
// #given no config files exist
|
|
290
|
-
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
291
|
-
delete process.env.XDG_CONFIG_HOME;
|
|
292
|
-
delete process.env.OPENCODE_CONFIG_DIR;
|
|
293
|
-
// #when detectExistingConfigDir is called
|
|
294
|
-
const result = detectExistingConfigDir('opencode', '1.0.200');
|
|
295
|
-
// #then result is either null or a valid string path
|
|
296
|
-
expect(result === null || typeof result === 'string').toBe(true);
|
|
297
|
-
});
|
|
298
|
-
test('includes OPENCODE_CONFIG_DIR in search locations when set', () => {
|
|
299
|
-
// #given OPENCODE_CONFIG_DIR is set to a custom path
|
|
300
|
-
process.env.OPENCODE_CONFIG_DIR = '/custom/opencode/path';
|
|
301
|
-
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
302
|
-
delete process.env.XDG_CONFIG_HOME;
|
|
303
|
-
// #when detectExistingConfigDir is called
|
|
304
|
-
const result = detectExistingConfigDir('opencode', '1.0.200');
|
|
305
|
-
// #then result is either null (no config file exists) or a valid string path
|
|
306
|
-
// The important thing is that the function doesn't throw
|
|
307
|
-
expect(result === null || typeof result === 'string').toBe(true);
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { Plugin } from '@opencode-ai/plugin';
|
|
2
|
-
import type { McpLocalConfig } from '@opencode-ai/sdk';
|
|
3
|
-
|
|
4
|
-
import { createBuiltinMcps } from './morph/mcps';
|
|
5
|
-
import { createModelRouterHook } from './morph/router';
|
|
6
|
-
|
|
7
|
-
const MorphOpenCodePlugin: Plugin = async () => {
|
|
8
|
-
const builtinMcps: Record<string, McpLocalConfig> = createBuiltinMcps();
|
|
9
|
-
const routerHook = createModelRouterHook();
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
config: async (currentConfig: any) => {
|
|
13
|
-
currentConfig.mcp = {
|
|
14
|
-
...currentConfig.mcp,
|
|
15
|
-
...builtinMcps,
|
|
16
|
-
};
|
|
17
|
-
},
|
|
18
|
-
...routerHook,
|
|
19
|
-
};
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export default MorphOpenCodePlugin;
|
package/src/morph/mcps.test.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
vi.mock('../shared/config', () => ({
|
|
4
|
-
API_KEY: 'test-api-key-123',
|
|
5
|
-
}));
|
|
6
|
-
|
|
7
|
-
import { createBuiltinMcps } from './mcps';
|
|
8
|
-
|
|
9
|
-
describe('mcps.ts', () => {
|
|
10
|
-
describe('createBuiltinMcps', () => {
|
|
11
|
-
it('should create morph_mcp configuration', () => {
|
|
12
|
-
const mcps = createBuiltinMcps();
|
|
13
|
-
|
|
14
|
-
expect(mcps).toHaveProperty('morph_mcp');
|
|
15
|
-
expect(mcps.morph_mcp.type).toBe('local');
|
|
16
|
-
expect(mcps.morph_mcp.enabled).toBe(true);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('should set correct command for morph_mcp', () => {
|
|
20
|
-
const mcps = createBuiltinMcps();
|
|
21
|
-
|
|
22
|
-
expect(mcps.morph_mcp.command).toEqual([
|
|
23
|
-
'npx',
|
|
24
|
-
'-y',
|
|
25
|
-
'@morphllm/morphmcp',
|
|
26
|
-
]);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should set MORPH_API_KEY in environment', () => {
|
|
30
|
-
const mcps = createBuiltinMcps();
|
|
31
|
-
const env = mcps.morph_mcp.environment;
|
|
32
|
-
|
|
33
|
-
expect(env).toBeDefined();
|
|
34
|
-
expect(env?.MORPH_API_KEY).toBe('test-api-key-123');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should set ENABLED_TOOLS environment variable', () => {
|
|
38
|
-
const mcps = createBuiltinMcps();
|
|
39
|
-
const env = mcps.morph_mcp.environment;
|
|
40
|
-
|
|
41
|
-
expect(env).toBeDefined();
|
|
42
|
-
expect(env?.ENABLED_TOOLS).toBe('edit_file,warpgrep_codebase_search');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should only create morph_mcp key', () => {
|
|
46
|
-
const mcps = createBuiltinMcps();
|
|
47
|
-
|
|
48
|
-
expect(Object.keys(mcps)).toEqual(['morph_mcp']);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
});
|
package/src/morph/mcps.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { McpLocalConfig } from '@opencode-ai/sdk';
|
|
2
|
-
import { API_KEY } from '../shared/config';
|
|
3
|
-
|
|
4
|
-
export function createBuiltinMcps(): Record<string, McpLocalConfig> {
|
|
5
|
-
return {
|
|
6
|
-
morph_mcp: {
|
|
7
|
-
type: 'local',
|
|
8
|
-
command: ['npx', '-y', '@morphllm/morphmcp'],
|
|
9
|
-
environment: {
|
|
10
|
-
MORPH_API_KEY: API_KEY,
|
|
11
|
-
ENABLED_TOOLS: 'edit_file,warpgrep_codebase_search',
|
|
12
|
-
},
|
|
13
|
-
enabled: true,
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export default createBuiltinMcps;
|
package/src/morph/router.test.ts
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'bun:test';
|
|
2
|
-
import type { Part } from '@opencode-ai/sdk';
|
|
3
|
-
|
|
4
|
-
vi.mock('@morphllm/morphsdk', () => ({
|
|
5
|
-
MorphClient: vi.fn().mockImplementation(() => ({
|
|
6
|
-
routers: {
|
|
7
|
-
raw: {
|
|
8
|
-
classify: vi.fn(),
|
|
9
|
-
},
|
|
10
|
-
},
|
|
11
|
-
})),
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
vi.mock('../shared/config', () => ({
|
|
15
|
-
API_KEY: 'sk-test-api-key-123',
|
|
16
|
-
MORPH_MODEL_EASY: 'easy/easy',
|
|
17
|
-
MORPH_MODEL_MEDIUM: 'medium/medium',
|
|
18
|
-
MORPH_MODEL_HARD: 'hard/hard',
|
|
19
|
-
MORPH_MODEL_DEFAULT: 'default/default',
|
|
20
|
-
MORPH_ROUTER_PROMPT_CACHING_AWARE: false,
|
|
21
|
-
MORPH_ROUTER_ENABLED: true,
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
import { createModelRouterHook, extractPromptText } from './router';
|
|
25
|
-
|
|
26
|
-
describe('router.ts', () => {
|
|
27
|
-
describe('extractPromptText', () => {
|
|
28
|
-
it('should extract text from parts', () => {
|
|
29
|
-
const parts: Part[] = [
|
|
30
|
-
{ type: 'text', text: 'Hello' },
|
|
31
|
-
{ type: 'text', text: 'World' },
|
|
32
|
-
] as any;
|
|
33
|
-
expect(extractPromptText(parts)).toBe('Hello World');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should handle empty parts', () => {
|
|
37
|
-
const parts: Part[] = [];
|
|
38
|
-
expect(extractPromptText(parts)).toBe('');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should filter out non-text parts', () => {
|
|
42
|
-
const parts: Part[] = [
|
|
43
|
-
{ type: 'text', text: 'Hello' },
|
|
44
|
-
{ type: 'other', text: 'Ignore' },
|
|
45
|
-
{ type: 'text', text: 'World' },
|
|
46
|
-
] as any;
|
|
47
|
-
expect(extractPromptText(parts)).toBe('Hello World');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('should handle parts with no text', () => {
|
|
51
|
-
const parts: Part[] = [
|
|
52
|
-
{ type: 'text', text: 'Hello' },
|
|
53
|
-
{ type: 'text' },
|
|
54
|
-
{ type: 'text', text: 'World' },
|
|
55
|
-
] as any;
|
|
56
|
-
expect(extractPromptText(parts)).toBe('Hello World');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should handle all non-text parts', () => {
|
|
60
|
-
const parts: Part[] = [
|
|
61
|
-
{ type: 'image', data: 'base64...' },
|
|
62
|
-
{ type: 'tool_use', name: 'bash' },
|
|
63
|
-
] as any;
|
|
64
|
-
expect(extractPromptText(parts)).toBe('');
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('createModelRouterHook', () => {
|
|
69
|
-
it('should return a chat.message hook', () => {
|
|
70
|
-
const hook = createModelRouterHook();
|
|
71
|
-
expect('chat.message' in hook).toBe(true);
|
|
72
|
-
expect(typeof hook['chat.message']).toBe('function');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should call the classifier with the correct input', async () => {
|
|
76
|
-
const hook = createModelRouterHook();
|
|
77
|
-
const classify = vi.fn().mockResolvedValue({ difficulty: 'easy' });
|
|
78
|
-
const input = {
|
|
79
|
-
sessionID: '123',
|
|
80
|
-
classify,
|
|
81
|
-
};
|
|
82
|
-
const output = {
|
|
83
|
-
message: {} as any,
|
|
84
|
-
parts: [{ type: 'text', text: 'test prompt' }],
|
|
85
|
-
} as any;
|
|
86
|
-
await hook['chat.message'](input as any, output);
|
|
87
|
-
expect(classify).toHaveBeenCalledWith({ input: 'test prompt' });
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('should assign the correct model based on difficulty', async () => {
|
|
91
|
-
const hook = createModelRouterHook();
|
|
92
|
-
const classify = vi.fn().mockResolvedValue({ difficulty: 'hard' });
|
|
93
|
-
const input = {
|
|
94
|
-
sessionID: '123',
|
|
95
|
-
classify,
|
|
96
|
-
};
|
|
97
|
-
const output = {
|
|
98
|
-
message: {} as any,
|
|
99
|
-
parts: [{ type: 'text', text: 'test prompt' }],
|
|
100
|
-
} as any;
|
|
101
|
-
await hook['chat.message'](input as any, output);
|
|
102
|
-
expect(input).toHaveProperty('model');
|
|
103
|
-
expect((input as any).model.providerID).toBe('hard');
|
|
104
|
-
expect((input as any).model.modelID).toBe('hard');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should use the default model if no difficulty is returned', async () => {
|
|
108
|
-
const hook = createModelRouterHook();
|
|
109
|
-
const classify = vi.fn().mockResolvedValue({});
|
|
110
|
-
const input = {
|
|
111
|
-
sessionID: '123',
|
|
112
|
-
classify,
|
|
113
|
-
};
|
|
114
|
-
const output = {
|
|
115
|
-
message: {} as any,
|
|
116
|
-
parts: [{ type: 'text', text: 'test prompt' }],
|
|
117
|
-
} as any;
|
|
118
|
-
await hook['chat.message'](input as any, output);
|
|
119
|
-
expect(input).toHaveProperty('model');
|
|
120
|
-
expect((input as any).model.providerID).toBe('default');
|
|
121
|
-
expect((input as any).model.modelID).toBe('default');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('should use default model when router returns nothing', async () => {
|
|
125
|
-
const hook = createModelRouterHook();
|
|
126
|
-
const classify = vi.fn().mockResolvedValue({});
|
|
127
|
-
const input = {
|
|
128
|
-
sessionID: '123',
|
|
129
|
-
classify,
|
|
130
|
-
};
|
|
131
|
-
const output = {
|
|
132
|
-
message: {} as any,
|
|
133
|
-
parts: [{ type: 'text', text: 'test prompt' }],
|
|
134
|
-
} as any;
|
|
135
|
-
await hook['chat.message'](input as any, output);
|
|
136
|
-
expect(input).toHaveProperty('model');
|
|
137
|
-
expect((input as any).model.providerID).toBe('default');
|
|
138
|
-
expect((input as any).model.modelID).toBe('default');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('should handle classifier throwing an error gracefully', async () => {
|
|
142
|
-
const hook = createModelRouterHook();
|
|
143
|
-
const classify = vi.fn().mockRejectedValue(new Error('API Error'));
|
|
144
|
-
const input = {
|
|
145
|
-
sessionID: '123',
|
|
146
|
-
classify,
|
|
147
|
-
};
|
|
148
|
-
const output = {
|
|
149
|
-
message: {} as any,
|
|
150
|
-
parts: [{ type: 'text', text: 'test prompt' }],
|
|
151
|
-
} as any;
|
|
152
|
-
|
|
153
|
-
await expect(hook['chat.message'](input as any, output)).rejects.toThrow(
|
|
154
|
-
'API Error'
|
|
155
|
-
);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('should handle classifier returning null', async () => {
|
|
159
|
-
const hook = createModelRouterHook();
|
|
160
|
-
const classify = vi.fn().mockResolvedValue(null);
|
|
161
|
-
const input = {
|
|
162
|
-
sessionID: '123',
|
|
163
|
-
classify,
|
|
164
|
-
};
|
|
165
|
-
const output = {
|
|
166
|
-
message: {} as any,
|
|
167
|
-
parts: [{ type: 'text', text: 'test prompt' }],
|
|
168
|
-
} as any;
|
|
169
|
-
await hook['chat.message'](input as any, output);
|
|
170
|
-
expect((input as any).model.providerID).toBe('default');
|
|
171
|
-
expect((input as any).model.modelID).toBe('default');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('should handle classifier returning undefined difficulty', async () => {
|
|
175
|
-
const hook = createModelRouterHook();
|
|
176
|
-
const classify = vi.fn().mockResolvedValue({ difficulty: undefined });
|
|
177
|
-
const input = {
|
|
178
|
-
sessionID: '123',
|
|
179
|
-
classify,
|
|
180
|
-
};
|
|
181
|
-
const output = {
|
|
182
|
-
message: {} as any,
|
|
183
|
-
parts: [{ type: 'text', text: 'test prompt' }],
|
|
184
|
-
} as any;
|
|
185
|
-
await hook['chat.message'](input as any, output);
|
|
186
|
-
expect((input as any).model.providerID).toBe('default');
|
|
187
|
-
expect((input as any).model.modelID).toBe('default');
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('should default to default model for invalid difficulty values', async () => {
|
|
191
|
-
const hook = createModelRouterHook();
|
|
192
|
-
const classify = vi.fn().mockResolvedValue({ difficulty: 'ultra-hard' });
|
|
193
|
-
const input = {
|
|
194
|
-
sessionID: '123',
|
|
195
|
-
classify,
|
|
196
|
-
};
|
|
197
|
-
const output = {
|
|
198
|
-
message: {} as any,
|
|
199
|
-
parts: [{ type: 'text', text: 'test prompt' }],
|
|
200
|
-
} as any;
|
|
201
|
-
await hook['chat.message'](input as any, output);
|
|
202
|
-
expect((input as any).model.providerID).toBe('default');
|
|
203
|
-
expect((input as any).model.modelID).toBe('default');
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('should default to default model for empty string difficulty', async () => {
|
|
207
|
-
const hook = createModelRouterHook();
|
|
208
|
-
const classify = vi.fn().mockResolvedValue({ difficulty: '' });
|
|
209
|
-
const input = {
|
|
210
|
-
sessionID: '123',
|
|
211
|
-
classify,
|
|
212
|
-
};
|
|
213
|
-
const output = {
|
|
214
|
-
message: {} as any,
|
|
215
|
-
parts: [{ type: 'text', text: 'test prompt' }],
|
|
216
|
-
} as any;
|
|
217
|
-
await hook['chat.message'](input as any, output);
|
|
218
|
-
expect((input as any).model.providerID).toBe('default');
|
|
219
|
-
expect((input as any).model.modelID).toBe('default');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('should handle case-insensitive difficulty matching', async () => {
|
|
223
|
-
const hook = createModelRouterHook();
|
|
224
|
-
const classify = vi.fn().mockResolvedValue({ difficulty: 'HARD' });
|
|
225
|
-
const input = {
|
|
226
|
-
sessionID: '123',
|
|
227
|
-
classify,
|
|
228
|
-
};
|
|
229
|
-
const output = {
|
|
230
|
-
message: {} as any,
|
|
231
|
-
parts: [{ type: 'text', text: 'test prompt' }],
|
|
232
|
-
} as any;
|
|
233
|
-
await hook['chat.message'](input as any, output);
|
|
234
|
-
expect((input as any).model.providerID).toBe('hard');
|
|
235
|
-
expect((input as any).model.modelID).toBe('hard');
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('should route all messages when MORPH_ROUTER_PROMPT_CACHING_AWARE is disabled', async () => {
|
|
239
|
-
const hook = createModelRouterHook();
|
|
240
|
-
const classify = vi
|
|
241
|
-
.fn()
|
|
242
|
-
.mockResolvedValueOnce({ difficulty: 'hard' })
|
|
243
|
-
.mockResolvedValueOnce({ difficulty: 'easy' });
|
|
244
|
-
|
|
245
|
-
const sessionID = 'session-456';
|
|
246
|
-
const input1 = { sessionID, classify };
|
|
247
|
-
const output1 = {
|
|
248
|
-
message: {} as any,
|
|
249
|
-
parts: [{ type: 'text', text: 'first message' }],
|
|
250
|
-
} as any;
|
|
251
|
-
|
|
252
|
-
await hook['chat.message'](input1 as any, output1);
|
|
253
|
-
expect(classify).toHaveBeenCalledTimes(1);
|
|
254
|
-
expect((input1 as any).model.providerID).toBe('hard');
|
|
255
|
-
|
|
256
|
-
const input2 = { sessionID, classify };
|
|
257
|
-
const output2 = {
|
|
258
|
-
message: {} as any,
|
|
259
|
-
parts: [{ type: 'text', text: 'second message' }],
|
|
260
|
-
} as any;
|
|
261
|
-
|
|
262
|
-
await hook['chat.message'](input2 as any, output2);
|
|
263
|
-
expect(classify).toHaveBeenCalledTimes(2);
|
|
264
|
-
expect((input2 as any).model.providerID).toBe('easy');
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
});
|