@@ -13,8 +13,9 @@ import type { WarpGrepResult, CompactResult } from "@morphllm/morphsdk";
1313import type { Part , TextPart , ToolPart , Message } from "@opencode-ai/sdk" ;
1414import { isAbsolute , resolve as resolvePath } from "node:path" ;
1515
16- // Config from environment — only MORPH_API_KEY is required
17- const MORPH_API_KEY = process . env . MORPH_API_KEY ;
16+ // API key from MORPH_API_KEY env var, or the `morph.apiKey` field in
17+ // opencode config (resolved during plugin init for desktop users).
18+ let MORPH_API_KEY = process . env . MORPH_API_KEY ;
1819const MORPH_API_URL = "https://api.morphllm.com" ;
1920const MORPH_TIMEOUT = 30000 ;
2021const MORPH_WARP_GREP_TIMEOUT = 60000 ;
@@ -75,37 +76,30 @@ const EXISTING_CODE_MARKER = "// ... existing code ...";
7576const MORPH_ROUTING_HINT_HEADER = "Morph plugin routing hints:" ;
7677
7778/**
78- * Shared MorphClient — FastApply uses morph.fastApply.applyEdit()
79- * with MORPH_API_URL passed as per-call override.
79+ * Morph SDK clients (FastApply, WarpGrep, Compact). Built by initMorphClients()
80+ * once MORPH_API_KEY is known — at module load for the env var, and again during
81+ * plugin init if the key comes from opencode config.
8082 */
81- const morph = MORPH_API_KEY
82- ? new MorphClient ( {
83- apiKey : MORPH_API_KEY ,
84- timeout : MORPH_TIMEOUT ,
85- } )
86- : null ;
83+ let morph : MorphClient | null = null ;
84+ let warpGrep : WarpGrepClient | null = null ;
85+ let compactClient : CompactClient | null = null ;
86+
87+ function initMorphClients ( ) {
88+ if ( ! MORPH_API_KEY ) return ;
89+ morph = new MorphClient ( { apiKey : MORPH_API_KEY , timeout : MORPH_TIMEOUT } ) ;
90+ warpGrep = new WarpGrepClient ( {
91+ morphApiKey : MORPH_API_KEY ,
92+ morphApiUrl : MORPH_API_URL ,
93+ timeout : MORPH_WARP_GREP_TIMEOUT ,
94+ } ) ;
95+ compactClient = new CompactClient ( {
96+ morphApiKey : MORPH_API_KEY ,
97+ morphApiUrl : MORPH_API_URL ,
98+ timeout : MORPH_COMPACT_TIMEOUT ,
99+ } ) ;
100+ }
87101
88- /**
89- * Separate WarpGrep client with its own timeout (typically longer than fast apply).
90- */
91- const warpGrep = MORPH_API_KEY
92- ? new WarpGrepClient ( {
93- morphApiKey : MORPH_API_KEY ,
94- morphApiUrl : MORPH_API_URL ,
95- timeout : MORPH_WARP_GREP_TIMEOUT ,
96- } )
97- : null ;
98-
99- /**
100- * Separate CompactClient for context compaction.
101- */
102- const compactClient = MORPH_API_KEY
103- ? new CompactClient ( {
104- morphApiKey : MORPH_API_KEY ,
105- morphApiUrl : MORPH_API_URL ,
106- timeout : MORPH_COMPACT_TIMEOUT ,
107- } )
108- : null ;
102+ initMorphClients ( ) ;
109103
110104/**
111105 * Model context window size in tokens. Updated from chat.params hook.
@@ -752,6 +746,17 @@ const MorphPlugin: Plugin = async ({ directory, worktree, client }) => {
752746 } catch { }
753747 } ;
754748
749+ // Fall back to the `morph.apiKey` field in opencode config (for desktop
750+ // users who can't set env vars). Env var still takes precedence.
751+ if ( ! MORPH_API_KEY ) {
752+ const cfg = await client . config ?. get ( ) . catch ( ( ) => null ) ;
753+ const key = ( cfg ?. data as { morph ?: { apiKey ?: string } } ) ?. morph ?. apiKey ;
754+ if ( key ) {
755+ MORPH_API_KEY = key ;
756+ initMorphClients ( ) ;
757+ }
758+ }
759+
755760 if ( ! MORPH_API_KEY ) {
756761 await log (
757762 "warn" ,
0 commit comments