opencode-morphllm 0.0.3 → 0.0.5

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
@@ -2,6 +2,10 @@
2
2
 
3
3
  This is an OpenCode Plugin for [MorphLLM](https://morphllm.com/). This plugin just adds in `edit_file` and `warpgrep_codebase_search` from MorphLLM to your agent configs as well as the intelligent model router for choosing different models based on the difficulty of the prompt.
4
4
 
5
+ Github: https://github.com/VitoLin/opencode-morphllm
6
+
7
+ NPM: https://www.npmjs.com/package/opencode-morphllm
8
+
5
9
  ## Installation
6
10
 
7
11
  In `~/.config/opencode/opencode.json`, add the following config:
@@ -12,7 +16,7 @@ In `~/.config/opencode/opencode.json`, add the following config:
12
16
  ]
13
17
  ```
14
18
 
15
- You can also set the `opencode-morphllm` config variables by creating a .env file at `~/.config/opencode/morph.json`. You can find the provider and model ids from https://models.dev
19
+ You can also set the `opencode-morphllm` config variables by creating a `json` file at `~/.config/opencode/morph.json`. You can find the provider and model ids from https://models.dev
16
20
 
17
21
  Example configs:
18
22
 
@@ -0,0 +1,20 @@
1
+ export declare function getMorphPluginConfigPath(): string;
2
+ interface MorphConfig {
3
+ MORPH_API_KEY?: string;
4
+ MORPH_ROUTER_ENABLED?: boolean;
5
+ MORPH_MODEL_EASY?: string;
6
+ MORPH_MODEL_MEDIUM?: string;
7
+ MORPH_MODEL_HARD?: string;
8
+ MORPH_MODEL_DEFAULT?: string;
9
+ }
10
+ export declare function loadMorphPluginConfig(): MorphConfig | null;
11
+ export declare function loadMorphPluginConfigWithProjectOverride(
12
+ projectDir?: string
13
+ ): MorphConfig;
14
+ export declare const API_KEY: string;
15
+ export declare const MORPH_MODEL_EASY: string;
16
+ export declare const MORPH_MODEL_MEDIUM: string;
17
+ export declare const MORPH_MODEL_HARD: string;
18
+ export declare const MORPH_MODEL_DEFAULT: string;
19
+ export declare const MORPH_ROUTER_ENABLED: boolean;
20
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,71 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getOpenCodeConfigDir } from './shared/opencode-config-dir';
4
+ const MORPH_PLUGIN_NAME = 'morph';
5
+ export function getMorphPluginConfigPath() {
6
+ const configDir = getOpenCodeConfigDir({ binary: 'opencode' });
7
+ return join(configDir, `${MORPH_PLUGIN_NAME}.json`);
8
+ }
9
+ function parseJsonc(content) {
10
+ try {
11
+ // A simple JSONC parser that removes comments
12
+ const cleanedContent = content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
13
+ return JSON.parse(cleanedContent);
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+ export function loadMorphPluginConfig() {
19
+ const jsoncPath = getMorphPluginConfigPath().replace('.json', '.jsonc');
20
+ const jsonPath = getMorphPluginConfigPath();
21
+ if (existsSync(jsoncPath)) {
22
+ try {
23
+ const content = readFileSync(jsoncPath, 'utf-8');
24
+ return parseJsonc(content);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ if (existsSync(jsonPath)) {
30
+ try {
31
+ const content = readFileSync(jsonPath, 'utf-8');
32
+ return JSON.parse(content);
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ export function loadMorphPluginConfigWithProjectOverride(
40
+ projectDir = process.cwd()
41
+ ) {
42
+ const userConfig = loadMorphPluginConfig() ?? {};
43
+ const projectBasePath = join(projectDir, '.opencode', MORPH_PLUGIN_NAME);
44
+ const projectJsoncPath = `${projectBasePath}.jsonc`;
45
+ const projectJsonPath = `${projectBasePath}.json`;
46
+ let projectConfig = {};
47
+ if (existsSync(projectJsoncPath)) {
48
+ try {
49
+ const content = readFileSync(projectJsoncPath, 'utf-8');
50
+ projectConfig = parseJsonc(content) ?? {};
51
+ } catch {
52
+ // Ignore parse errors
53
+ }
54
+ } else if (existsSync(projectJsonPath)) {
55
+ try {
56
+ const content = readFileSync(projectJsonPath, 'utf-8');
57
+ projectConfig = JSON.parse(content);
58
+ } catch {
59
+ // Ignore parse errors
60
+ }
61
+ }
62
+ return { ...userConfig, ...projectConfig };
63
+ }
64
+ const config = loadMorphPluginConfigWithProjectOverride();
65
+ export const API_KEY = config.MORPH_API_KEY || '';
66
+ export const MORPH_MODEL_EASY = config.MORPH_MODEL_EASY || '';
67
+ export const MORPH_MODEL_MEDIUM = config.MORPH_MODEL_MEDIUM || '';
68
+ export const MORPH_MODEL_HARD = config.MORPH_MODEL_HARD || '';
69
+ export const MORPH_MODEL_DEFAULT =
70
+ config.MORPH_MODEL_DEFAULT || MORPH_MODEL_MEDIUM;
71
+ export const MORPH_ROUTER_ENABLED = config.MORPH_ROUTER_ENABLED ?? true;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createBuiltinMcps } from './mcps';
2
2
  import { createModelRouterHook } from './router';
3
- import { MORPH_ROUTER_ENABLED } from './const';
3
+ import { MORPH_ROUTER_ENABLED } from './config';
4
4
  const MorphOpenCodePlugin = async () => {
5
5
  const builtinMcps = createBuiltinMcps();
6
6
  const routerHook = MORPH_ROUTER_ENABLED ? createModelRouterHook() : {};
package/dist/mcps.js CHANGED
@@ -1,4 +1,4 @@
1
- import { API_KEY } from './const';
1
+ import { API_KEY } from './config';
2
2
  export function createBuiltinMcps() {
3
3
  return {
4
4
  morph_mcp: {
package/dist/router.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  MORPH_MODEL_MEDIUM,
6
6
  MORPH_MODEL_HARD,
7
7
  MORPH_MODEL_DEFAULT,
8
- } from './const';
8
+ } from './config';
9
9
  const morph = new MorphClient({ apiKey: API_KEY });
10
10
  function parseModel(s) {
11
11
  if (!s) return { providerID: '', modelID: '' };
@@ -38,7 +38,6 @@ export function createModelRouterHook() {
38
38
  const chosen = pickModelForDifficulty(classification?.difficulty);
39
39
  const finalProviderID = chosen.providerID || input.model.providerID;
40
40
  const finalModelID = chosen.modelID || input.model.modelID;
41
-
42
41
  input.model.providerID = finalProviderID;
43
42
  input.model.modelID = finalModelID;
44
43
  },
@@ -0,0 +1,9 @@
1
+ interface OpenCodeConfigDirOptions {
2
+ binary: 'opencode' | 'opencode-desktop';
3
+ version?: string | null;
4
+ checkExisting?: boolean;
5
+ }
6
+ export declare function getOpenCodeConfigDir(
7
+ options: OpenCodeConfigDirOptions
8
+ ): string;
9
+ export {};
@@ -0,0 +1,56 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join, resolve } from 'node:path';
4
+ function getTauriConfigDir(identifier) {
5
+ const platform = process.platform;
6
+ switch (platform) {
7
+ case 'darwin':
8
+ return join(homedir(), 'Library', 'Application Support', identifier);
9
+ case 'win32': {
10
+ const appData =
11
+ process.env.APPDATA || join(homedir(), 'AppData', 'Roaming');
12
+ return join(appData, identifier);
13
+ }
14
+ case 'linux':
15
+ default: {
16
+ const xdgConfig =
17
+ process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
18
+ return join(xdgConfig, identifier);
19
+ }
20
+ }
21
+ }
22
+ function getCliConfigDir() {
23
+ const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
24
+ if (envConfigDir) {
25
+ return resolve(envConfigDir);
26
+ }
27
+ if (process.platform === 'win32') {
28
+ const crossPlatformDir = join(homedir(), '.config', 'opencode');
29
+ const crossPlatformConfig = join(crossPlatformDir, 'opencode.json');
30
+ if (existsSync(crossPlatformConfig)) {
31
+ return crossPlatformDir;
32
+ }
33
+ const appData =
34
+ process.env.APPDATA || join(homedir(), 'AppData', 'Roaming');
35
+ const appdataDir = join(appData, 'opencode');
36
+ const appdataConfig = join(appdataDir, 'opencode.json');
37
+ if (existsSync(appdataConfig)) {
38
+ return appdataDir;
39
+ }
40
+ return crossPlatformDir;
41
+ }
42
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
43
+ return join(xdgConfig, 'opencode');
44
+ }
45
+ export function getOpenCodeConfigDir(options) {
46
+ if (options.binary === 'opencode-desktop') {
47
+ const version = options.version;
48
+ const isDev =
49
+ !!version && (version.includes('-dev') || version.includes('.dev'));
50
+ const identifier = isDev
51
+ ? 'ai.opencode.desktop.dev'
52
+ : 'ai.opencode.desktop';
53
+ return getTauriConfigDir(identifier);
54
+ }
55
+ return getCliConfigDir();
56
+ }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "opencode-morphllm",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "author": "Vito Lin",
5
5
  "main": "dist/index.js",
6
6
  "description": "OpenCode plugin for MorphLLM",
7
7
  "scripts": {
8
- "build": "tsc -p tsconfig.json",
8
+ "build": "rm -rf dist && tsc -p tsconfig.json",
9
9
  "test": "bun test",
10
10
  "format": "prettier --write .",
11
11
  "format:check": "prettier --check .",
package/src/config.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getOpenCodeConfigDir } from './shared/opencode-config-dir';
4
+
5
+ const MORPH_PLUGIN_NAME = 'morph';
6
+
7
+ export function getMorphPluginConfigPath(): string {
8
+ const configDir = getOpenCodeConfigDir({ binary: 'opencode' });
9
+ return join(configDir, `${MORPH_PLUGIN_NAME}.json`);
10
+ }
11
+
12
+ interface MorphConfig {
13
+ MORPH_API_KEY?: string;
14
+ MORPH_ROUTER_ENABLED?: boolean;
15
+ MORPH_MODEL_EASY?: string;
16
+ MORPH_MODEL_MEDIUM?: string;
17
+ MORPH_MODEL_HARD?: string;
18
+ MORPH_MODEL_DEFAULT?: string;
19
+ }
20
+
21
+ function parseJsonc<T>(content: string): T | null {
22
+ try {
23
+ // A simple JSONC parser that removes comments
24
+ const cleanedContent = content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
25
+ return JSON.parse(cleanedContent) as T;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ export function loadMorphPluginConfig(): MorphConfig | null {
32
+ const jsoncPath = getMorphPluginConfigPath().replace('.json', '.jsonc');
33
+ const jsonPath = getMorphPluginConfigPath();
34
+
35
+ if (existsSync(jsoncPath)) {
36
+ try {
37
+ const content = readFileSync(jsoncPath, 'utf-8');
38
+ return parseJsonc<MorphConfig>(content);
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ if (existsSync(jsonPath)) {
45
+ try {
46
+ const content = readFileSync(jsonPath, 'utf-8');
47
+ return JSON.parse(content) as MorphConfig;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ return null;
54
+ }
55
+
56
+ export function loadMorphPluginConfigWithProjectOverride(
57
+ projectDir: string = process.cwd()
58
+ ): MorphConfig {
59
+ const userConfig = loadMorphPluginConfig() ?? {};
60
+
61
+ const projectBasePath = join(projectDir, '.opencode', MORPH_PLUGIN_NAME);
62
+ const projectJsoncPath = `${projectBasePath}.jsonc`;
63
+ const projectJsonPath = `${projectBasePath}.json`;
64
+
65
+ let projectConfig: MorphConfig = {};
66
+
67
+ if (existsSync(projectJsoncPath)) {
68
+ try {
69
+ const content = readFileSync(projectJsoncPath, 'utf-8');
70
+ projectConfig = parseJsonc<MorphConfig>(content) ?? {};
71
+ } catch {
72
+ // Ignore parse errors
73
+ }
74
+ } else if (existsSync(projectJsonPath)) {
75
+ try {
76
+ const content = readFileSync(projectJsonPath, 'utf-8');
77
+ projectConfig = JSON.parse(content) as MorphConfig;
78
+ } catch {
79
+ // Ignore parse errors
80
+ }
81
+ }
82
+
83
+ return { ...userConfig, ...projectConfig };
84
+ }
85
+
86
+ const config = loadMorphPluginConfigWithProjectOverride();
87
+
88
+ export const API_KEY = config.MORPH_API_KEY || '';
89
+ export const MORPH_MODEL_EASY = config.MORPH_MODEL_EASY || '';
90
+ export const MORPH_MODEL_MEDIUM = config.MORPH_MODEL_MEDIUM || '';
91
+ export const MORPH_MODEL_HARD = config.MORPH_MODEL_HARD || '';
92
+ export const MORPH_MODEL_DEFAULT =
93
+ config.MORPH_MODEL_DEFAULT || MORPH_MODEL_MEDIUM;
94
+ export const MORPH_ROUTER_ENABLED = config.MORPH_ROUTER_ENABLED ?? true;
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ import type { McpLocalConfig } from '@opencode-ai/sdk';
3
3
 
4
4
  import { createBuiltinMcps } from './mcps';
5
5
  import { createModelRouterHook } from './router';
6
- import { MORPH_ROUTER_ENABLED } from './const';
6
+ import { MORPH_ROUTER_ENABLED } from './config';
7
7
 
8
8
  const MorphOpenCodePlugin: Plugin = async () => {
9
9
  const builtinMcps: Record<string, McpLocalConfig> = createBuiltinMcps();
package/src/mcps.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { McpLocalConfig } from '@opencode-ai/sdk';
2
- import { API_KEY } from './const';
2
+ import { API_KEY } from './config';
3
3
 
4
4
  export function createBuiltinMcps(): Record<string, McpLocalConfig> {
5
5
  return {
package/src/router.ts CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  MORPH_MODEL_MEDIUM,
7
7
  MORPH_MODEL_HARD,
8
8
  MORPH_MODEL_DEFAULT,
9
- } from './const';
9
+ } from './config';
10
10
  import type { Part, UserMessage } from '@opencode-ai/sdk';
11
11
  import type {
12
12
  RouterInput,
@@ -69,10 +69,6 @@ export function createModelRouterHook() {
69
69
  const finalProviderID = chosen.providerID || input.model.providerID;
70
70
  const finalModelID = chosen.modelID || input.model.modelID;
71
71
 
72
- console.debug(
73
- `[Morph Router] Prompt classified as difficulty: ${classification?.difficulty}. Routing to model: ${finalProviderID}/${finalModelID}`
74
- );
75
-
76
72
  input.model.providerID = finalProviderID;
77
73
  input.model.modelID = finalModelID;
78
74
  },
@@ -0,0 +1,77 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join, resolve } from 'node:path';
4
+
5
+ interface OpenCodeConfigDirOptions {
6
+ binary: 'opencode' | 'opencode-desktop';
7
+ version?: string | null;
8
+ checkExisting?: boolean;
9
+ }
10
+
11
+ function getTauriConfigDir(identifier: string): string {
12
+ const platform = process.platform;
13
+
14
+ switch (platform) {
15
+ case 'darwin':
16
+ return join(homedir(), 'Library', 'Application Support', identifier);
17
+
18
+ case 'win32': {
19
+ const appData =
20
+ process.env.APPDATA || join(homedir(), 'AppData', 'Roaming');
21
+ return join(appData, identifier);
22
+ }
23
+
24
+ case 'linux':
25
+ default: {
26
+ const xdgConfig =
27
+ process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
28
+ return join(xdgConfig, identifier);
29
+ }
30
+ }
31
+ }
32
+
33
+ function getCliConfigDir(): string {
34
+ const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
35
+ if (envConfigDir) {
36
+ return resolve(envConfigDir);
37
+ }
38
+
39
+ if (process.platform === 'win32') {
40
+ const crossPlatformDir = join(homedir(), '.config', 'opencode');
41
+ const crossPlatformConfig = join(crossPlatformDir, 'opencode.json');
42
+
43
+ if (existsSync(crossPlatformConfig)) {
44
+ return crossPlatformDir;
45
+ }
46
+
47
+ const appData =
48
+ process.env.APPDATA || join(homedir(), 'AppData', 'Roaming');
49
+ const appdataDir = join(appData, 'opencode');
50
+ const appdataConfig = join(appdataDir, 'opencode.json');
51
+
52
+ if (existsSync(appdataConfig)) {
53
+ return appdataDir;
54
+ }
55
+
56
+ return crossPlatformDir;
57
+ }
58
+
59
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
60
+ return join(xdgConfig, 'opencode');
61
+ }
62
+
63
+ export function getOpenCodeConfigDir(
64
+ options: OpenCodeConfigDirOptions
65
+ ): string {
66
+ if (options.binary === 'opencode-desktop') {
67
+ const version = options.version;
68
+ const isDev =
69
+ !!version && (version.includes('-dev') || version.includes('.dev'));
70
+ const identifier = isDev
71
+ ? 'ai.opencode.desktop.dev'
72
+ : 'ai.opencode.desktop';
73
+ return getTauriConfigDir(identifier);
74
+ }
75
+
76
+ return getCliConfigDir();
77
+ }
package/dist/const.d.ts DELETED
@@ -1,6 +0,0 @@
1
- export declare const API_KEY: string;
2
- export declare const MORPH_MODEL_EASY: string;
3
- export declare const MORPH_MODEL_MEDIUM: string;
4
- export declare const MORPH_MODEL_HARD: string;
5
- export declare const MORPH_MODEL_DEFAULT: string;
6
- export declare const MORPH_ROUTER_ENABLED: boolean;
package/dist/const.js DELETED
@@ -1,20 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- const configPath = path.join(
4
- process.env.HOME || process.env.USERPROFILE || '',
5
- '.config/opencode/morph.json'
6
- );
7
- let config = {};
8
- try {
9
- if (fs.existsSync(configPath)) {
10
- config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
11
- }
12
- } catch (e) {
13
- console.warn('[Morph Plugin] Failed to load config file:', configPath);
14
- }
15
- export const API_KEY = config.MORPH_API_KEY || '';
16
- export const MORPH_MODEL_EASY = config.MORPH_MODEL_EASY || '';
17
- export const MORPH_MODEL_MEDIUM = config.MORPH_MODEL_MEDIUM || '';
18
- export const MORPH_MODEL_HARD = config.MORPH_MODEL_HARD || '';
19
- export const MORPH_MODEL_DEFAULT = config.MORPH_MODEL_DEFAULT || '';
20
- export const MORPH_ROUTER_ENABLED = config.MORPH_ROUTER_ENABLED ?? true;
package/src/const.ts DELETED
@@ -1,32 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- interface MorphConfig {
4
- MORPH_API_KEY?: string;
5
- MORPH_ROUTER_ENABLED?: boolean;
6
- MORPH_MODEL_EASY?: string;
7
- MORPH_MODEL_MEDIUM?: string;
8
- MORPH_MODEL_HARD?: string;
9
- MORPH_MODEL_DEFAULT?: string;
10
- }
11
-
12
- const configPath = path.join(
13
- process.env.HOME || process.env.USERPROFILE || '',
14
- '.config/opencode/morph.json'
15
- );
16
-
17
- let config: MorphConfig = {};
18
-
19
- try {
20
- if (fs.existsSync(configPath)) {
21
- config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
22
- }
23
- } catch (e) {
24
- console.warn('[Morph Plugin] Failed to load config file:', configPath);
25
- }
26
-
27
- export const API_KEY = config.MORPH_API_KEY || '';
28
- export const MORPH_MODEL_EASY = config.MORPH_MODEL_EASY || '';
29
- export const MORPH_MODEL_MEDIUM = config.MORPH_MODEL_MEDIUM || '';
30
- export const MORPH_MODEL_HARD = config.MORPH_MODEL_HARD || '';
31
- export const MORPH_MODEL_DEFAULT = config.MORPH_MODEL_DEFAULT || '';
32
- export const MORPH_ROUTER_ENABLED = config.MORPH_ROUTER_ENABLED ?? true;