Skip to content

Commit 7862770

Browse files
authored
Add plan-based Claude/OpenAI connections with OAuth (#509)
* feat: Add OpenAI Codex provider and OAuth integration - Introduced OpenAI Codex as a new provider type with OAuth support. - Implemented authentication flow including token exchange and callback server. - Updated settings schema to include Codex provider and associated OAuth fields. - Enhanced chat and provider management to accommodate Codex integration. - Added migration logic to transition existing settings to support the new provider. - Comprehensive tests added for migration and Codex functionality. * feat: Enhance HTTP transport for Codex integration - Added comments explaining the use of Node's http/https modules for Codex endpoints due to CORS restrictions. - Updated the nodePost function to throw an error when called on mobile platforms, ensuring proper platform handling. - Refactored the import of IncomingMessage type for better clarity and consistency. * feat: Add Anthropic Claude Code provider with OAuth support - Introduced the Anthropic Claude Code provider, including OAuth integration for authentication. - Implemented the necessary authentication flow, including token exchange and authorization URL generation. - Updated settings schema to include the new provider type and associated OAuth fields. - Enhanced provider management and chat model handling to accommodate Claude Code integration. - Added migration logic to transition existing settings to support the new provider. - Comprehensive tests added for migration and Claude Code functionality. * chore: Update OpenAI package and refactor message adapters - Upgraded OpenAI package from version 4.91.1 to 6.8.1 in package.json and package-lock.json. - Refactored DeepSeekMessageAdapter and PerplexityMessageAdapter to normalize tool calls using the new method. - Enhanced OpenAIMessageAdapter with a new normalizeToolCalls method to streamline tool call handling. - Updated NoStainlessOpenAI to improve header management in requests. * feat: Add OpenAI Codex model settings and reasoning support - Introduced settings for the OpenAI Codex model, allowing users to configure reasoning effort and summary. - Enhanced CodexMessageAdapter to handle reasoning summary extraction and inclusion in responses. - Updated OpenAICodexProvider to normalize requests with reasoning parameters. - Modified chat model schema to accommodate new reasoning fields. - Added utility functions for reasoning summary extraction in response payloads. * feat: Update ChatModelSettings to support new Anthropic Claude Code model * feat: Refactor provider settings and enhance migration logic for OpenAI and Anthropic models - Reintroduced OpenAI provider settings in the constants file for consistency. - Updated DEFAULT_PROVIDERS and DEFAULT_CHAT_MODELS to include new models for OpenAI Codex and Anthropic Claude Code. - Enhanced migration logic to ensure proper transition to version 15, incorporating new default providers and chat models. * feat: Refactor provider types and update settings for OpenAI and Anthropic models - Replaced 'openai-codex' and 'anthropic-claude-code' with 'openai-plan' and 'anthropic-plan' in constants and provider settings. - Updated DEFAULT_PROVIDERS and DEFAULT_CHAT_MODELS to reflect new provider types. - Adjusted migration logic to ensure compatibility with the new provider types. - Enhanced related components and schemas to support the updated provider structure. * feat: Implement subscription connection settings for OpenAI and Anthropic plans - Added new PlanConnectionsSection to manage connections for OpenAI and Claude subscriptions. - Introduced ConnectClaudePlanModal and ConnectOpenAIPlanModal for OAuth authentication flows. - Enhanced styles for plan connection cards and status indicators. - Updated SettingsTabRoot to include the new PlanConnectionsSection. - Refactored ProvidersSection to exclude subscription providers from API key management. * feat: Add PLAN_PROVIDER_TYPES constant and update provider filtering logic * refactor: Clean up provider connection logic and improve formatting * chore: Update .gitignore to exclude CLAUDE.md * style: Enhance layout and descriptions in PlanConnectionsSection * fix: Resolve coderabbit review - Refactored URL parameter extraction to support dynamic keys for better reusability. - Added state validation to ensure OAuth state consistency during the connection process. - Updated error messages for clearer user guidance in case of missing or mismatched OAuth states. - Introduced a new utility function for posting form data to streamline token requests. - Enhanced HTTP transport methods to support form URL encoding and improved error handling. * chore: Update column header in ProvidersSection from 'Credential' to 'API Key' * chore: Update README for v1.2.7 release * test: Enhance migration tests for v14 to v15
1 parent c8cf366 commit 7862770

37 files changed

+3113
-115
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,7 @@ meta.json
2020
data.json
2121

2222
# Exclude macOS Finder (System Explorer) View States
23-
.DS_Store
23+
.DS_Store
24+
25+
# Claude Code
26+
CLAUDE.md

README.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
</p>
1010

1111
> [!NOTE]
12-
> **✨ What's New in v1.2.6**
13-
> - Added support for latest AI models: GPT-5.2, Opus 4.5, Gemini 3, and xAI's Grok 4.1
14-
>
15-
> **🚀 New Feature: Model Context Protocol (MCP) is now available!**
16-
> You can now connect Smart Composer to external AI tools and using the open MCP standard.
12+
> **What's New**
13+
>
14+
> **v1.2.7** — Connect your Claude or OpenAI account directly (no API key required)
15+
>
16+
> **v1.2.6** — Support for GPT-5.2, Opus 4.5, Gemini 3, and Grok 4.1
17+
>
18+
> **🔌 MCP Support** — Connect Smart Composer to external tools and data sources via the [Model Context Protocol](https://modelcontextprotocol.io)
1719
1820
> [!WARNING]
1921
> **⚠️ Maintenance Notice**
@@ -83,7 +85,7 @@ MCP lets you use powerful third-party tools and data sources right inside your c
8385

8486
### Additional Features
8587

86-
- **Custom Model Selection**: Use your own model by setting your API Key (stored locally). Supported providers:
88+
- **Custom Model Selection**: Use your own model by setting your API Key (stored locally). Supported API providers:
8789
- OpenAI
8890
- Anthropic
8991
- Google (Gemini)
@@ -117,11 +119,12 @@ MCP lets you use powerful third-party tools and data sources right inside your c
117119
2. Navigate to "Community plugins" and click "Browse"
118120
3. Search for "Smart Composer" and click Install
119121
4. Enable the plugin in Community plugins
120-
5. Set up your API key in plugin settings
121-
- OpenAI : [ChatGPT API Keys](https://platform.openai.com/api-keys)
122-
- Anthropic : [Claude API Keys](https://console.anthropic.com/settings/keys)
123-
- Gemini : [Gemini API Keys](https://aistudio.google.com/apikey)
124-
- Groq : [Groq API Keys](https://console.groq.com/keys)
122+
5. Set up Smart Composer in plugin settings:
123+
- **Connect subscription (no API key)**: Connect your Claude/OpenAI account in `Settings > Smart Composer > Connect your subscription`
124+
- **API Providers (usage-based billing)**: Add an API key in `Settings > Smart Composer > Providers`
125+
- OpenAI: [ChatGPT API Keys](https://platform.openai.com/api-keys)
126+
- Anthropic: [Claude API Keys](https://console.anthropic.com/settings/keys)
127+
- Gemini: [Gemini API Keys](https://aistudio.google.com/apikey)
125128

126129
> [!TIP]
127130
> **Looking for a free option?**

package-lock.json

Lines changed: 42 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"lodash.isequal": "^4.5.0",
7070
"lucide-react": "^0.447.0",
7171
"minimatch": "^10.0.1",
72-
"openai": "^4.91.1",
72+
"openai": "^6.8.1",
7373
"parse5": "^7.1.2",
7474
"path-browserify": "^1.0.1",
7575
"react": "^18.3.1",

src/components/chat-view/Chat.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export type ChatProps = {
8585

8686
const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
8787
const app = useApp()
88-
const { settings } = useSettings()
88+
const { settings, setSettings } = useSettings()
8989
const { getRAGEngine } = useRAG()
9090
const { getMcpManager } = useMcp()
9191

@@ -293,8 +293,9 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
293293
const activeFileContent = await readTFileContent(activeFile, app.vault)
294294

295295
const { providerClient, model } = getChatModelClient({
296-
settings,
297296
modelId: settings.applyModelId,
297+
settings,
298+
setSettings,
298299
})
299300

300301
const updatedFileContent = await applyChangesToFile({

src/components/chat-view/useChatStreamManager.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,9 @@ export function useChatStreamManager({
5353
const { providerClient, model } = useMemo(() => {
5454
try {
5555
return getChatModelClient({
56-
settings,
5756
modelId: settings.chatModelId,
57+
settings,
58+
setSettings,
5859
})
5960
} catch (error) {
6061
if (error instanceof LLMModelNotFoundException) {
@@ -76,8 +77,9 @@ export function useChatStreamManager({
7677
),
7778
})
7879
return getChatModelClient({
79-
settings,
8080
modelId: firstChatModel.id,
81+
settings,
82+
setSettings,
8183
})
8284
}
8385
throw error

src/components/settings/SettingsTabRoot.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ChatSection } from './sections/ChatSection'
88
import { EtcSection } from './sections/EtcSection'
99
import { McpSection } from './sections/McpSection'
1010
import { ModelsSection } from './sections/ModelsSection'
11+
import { PlanConnectionsSection } from './sections/PlanConnectionsSection'
1112
import { ProvidersSection } from './sections/ProvidersSection'
1213
import { RAGSection } from './sections/RAGSection'
1314
import { TemplateSection } from './sections/TemplateSection'
@@ -34,6 +35,7 @@ export function SettingsTabRoot({ app, plugin }: SettingsTabRootProps) {
3435
cta
3536
/>
3637
</ObsidianSetting>
38+
<PlanConnectionsSection app={app} plugin={plugin} />
3739
<ChatSection />
3840
<ProvidersSection app={app} plugin={plugin} />
3941
<ModelsSection app={app} plugin={plugin} />
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { App, Notice } from 'obsidian'
2+
import { useEffect, useState } from 'react'
3+
4+
import { PROVIDER_TYPES_INFO } from '../../../constants'
5+
import {
6+
buildClaudeCodeAuthorizeUrl,
7+
exchangeClaudeCodeForTokens,
8+
generateClaudeCodePkce,
9+
generateClaudeCodeState,
10+
} from '../../../core/llm/claudeCodeAuth'
11+
import SmartComposerPlugin from '../../../main'
12+
import { ObsidianButton } from '../../common/ObsidianButton'
13+
import { ObsidianSetting } from '../../common/ObsidianSetting'
14+
import { ObsidianTextInput } from '../../common/ObsidianTextInput'
15+
import { ReactModal } from '../../common/ReactModal'
16+
17+
type ConnectClaudePlanModalProps = {
18+
plugin: SmartComposerPlugin
19+
onClose: () => void
20+
}
21+
22+
const CLAUDE_PLAN_PROVIDER_ID = PROVIDER_TYPES_INFO['anthropic-plan']
23+
.defaultProviderId as string
24+
25+
export class ConnectClaudePlanModal extends ReactModal<ConnectClaudePlanModalProps> {
26+
constructor(app: App, plugin: SmartComposerPlugin) {
27+
super({
28+
app: app,
29+
Component: ConnectClaudePlanModalComponent,
30+
props: { plugin },
31+
options: {
32+
title: 'Connect Claude subscription',
33+
},
34+
})
35+
}
36+
}
37+
38+
function ConnectClaudePlanModalComponent({
39+
plugin,
40+
onClose,
41+
}: ConnectClaudePlanModalProps) {
42+
const [authorizeUrl, setAuthorizeUrl] = useState('')
43+
const [code, setCode] = useState('')
44+
const [pkceVerifier, setPkceVerifier] = useState('')
45+
const [state, setState] = useState('')
46+
const [isConnecting, setIsConnecting] = useState(false)
47+
48+
const hasAuthData = authorizeUrl.length > 0 && pkceVerifier.length > 0
49+
50+
useEffect(() => {
51+
;(async () => {
52+
try {
53+
const pkce = await generateClaudeCodePkce()
54+
const newState = generateClaudeCodeState()
55+
const url = buildClaudeCodeAuthorizeUrl({ pkce, state: newState })
56+
setPkceVerifier(pkce.verifier)
57+
setState(newState)
58+
setAuthorizeUrl(url)
59+
} catch {
60+
new Notice('Failed to initialize OAuth flow')
61+
}
62+
})()
63+
}, [])
64+
65+
const connect = async () => {
66+
if (isConnecting) return
67+
if (!hasAuthData) {
68+
new Notice('OAuth link is not ready. Try again.')
69+
return
70+
}
71+
if (!code) {
72+
new Notice('Paste the authorization code')
73+
return
74+
}
75+
76+
try {
77+
setIsConnecting(true)
78+
79+
const tokens = await exchangeClaudeCodeForTokens({
80+
code,
81+
pkceVerifier,
82+
state,
83+
})
84+
85+
if (
86+
!plugin.settings.providers.find(
87+
(p) =>
88+
p.type === 'anthropic-plan' && p.id === CLAUDE_PLAN_PROVIDER_ID,
89+
)
90+
) {
91+
throw new Error('Claude Plan provider not found.')
92+
}
93+
await plugin.setSettings({
94+
...plugin.settings,
95+
providers: plugin.settings.providers.map((p) => {
96+
if (p.type === 'anthropic-plan' && p.id === CLAUDE_PLAN_PROVIDER_ID) {
97+
return {
98+
...p,
99+
oauth: {
100+
accessToken: tokens.access_token,
101+
refreshToken: tokens.refresh_token,
102+
expiresAt: Date.now() + (tokens.expires_in ?? 3600) * 1000,
103+
},
104+
}
105+
}
106+
return p
107+
}),
108+
})
109+
110+
new Notice('Claude Plan connected')
111+
onClose()
112+
} catch {
113+
new Notice('OAuth failed. Double-check the code and try again.')
114+
} finally {
115+
setIsConnecting(false)
116+
}
117+
}
118+
119+
return (
120+
<div>
121+
<div className="smtcmp-plan-connect-steps">
122+
<div className="smtcmp-plan-connect-steps-title">How it works</div>
123+
<ol>
124+
<li>Login to Claude in your browser</li>
125+
<li>Copy the code from the redirected URL</li>
126+
<li>Paste it here and click &quot;Connect&quot;</li>
127+
</ol>
128+
</div>
129+
130+
<ObsidianSetting
131+
name="Claude login"
132+
desc="Login to Claude Code in your browser."
133+
>
134+
<ObsidianButton
135+
text="Login to Claude"
136+
disabled={!authorizeUrl || isConnecting}
137+
onClick={() => {
138+
if (!authorizeUrl) return
139+
window.open(authorizeUrl, '_blank')
140+
}}
141+
cta
142+
/>
143+
</ObsidianSetting>
144+
145+
<ObsidianSetting
146+
name="Authorization code"
147+
desc="Paste the code from the redirected URL."
148+
required
149+
>
150+
<ObsidianTextInput
151+
value={code}
152+
placeholder="Paste authorization code"
153+
onChange={(value) => setCode(value)}
154+
/>
155+
</ObsidianSetting>
156+
157+
<ObsidianSetting>
158+
<ObsidianButton
159+
text="Connect"
160+
onClick={() => void connect()}
161+
disabled={isConnecting}
162+
cta
163+
/>
164+
<ObsidianButton
165+
text="Cancel"
166+
onClick={onClose}
167+
disabled={isConnecting}
168+
/>
169+
</ObsidianSetting>
170+
</div>
171+
)
172+
}

0 commit comments

Comments
 (0)