Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0dce449
feat: Add OpenAI Codex provider and OAuth integration
kevin-on Jan 17, 2026
1e727f1
feat: Enhance HTTP transport for Codex integration
kevin-on Jan 17, 2026
71bbb96
feat: Add Anthropic Claude Code provider with OAuth support
kevin-on Jan 17, 2026
3cea4e3
chore: Update OpenAI package and refactor message adapters
kevin-on Jan 17, 2026
eb11f00
feat: Add OpenAI Codex model settings and reasoning support
kevin-on Jan 18, 2026
6d24448
feat: Update ChatModelSettings to support new Anthropic Claude Code m…
kevin-on Jan 18, 2026
2dcd0cf
feat: Refactor provider settings and enhance migration logic for Open…
kevin-on Jan 18, 2026
51629bf
feat: Refactor provider types and update settings for OpenAI and Anth…
kevin-on Jan 18, 2026
e5ee235
feat: Implement subscription connection settings for OpenAI and Anthr…
kevin-on Jan 19, 2026
064bbfe
feat: Add PLAN_PROVIDER_TYPES constant and update provider filtering …
kevin-on Jan 19, 2026
1af22b1
refactor: Clean up provider connection logic and improve formatting
kevin-on Jan 19, 2026
7c1b0fa
chore: Update .gitignore to exclude CLAUDE.md
kevin-on Jan 19, 2026
1dc9cb0
style: Enhance layout and descriptions in PlanConnectionsSection
kevin-on Jan 19, 2026
ee4d21e
fix: Resolve coderabbit review
kevin-on Jan 19, 2026
b0bd0fd
chore: Update column header in ProvidersSection from 'Credential' to …
kevin-on Jan 19, 2026
b6e968b
chore: Update README for v1.2.7 release
kevin-on Jan 19, 2026
6d54f38
test: Enhance migration tests for v14 to v15
kevin-on Jan 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ meta.json
data.json

# Exclude macOS Finder (System Explorer) View States
.DS_Store
.DS_Store

# Claude Code
CLAUDE.md
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
</p>

> [!NOTE]
> **✨ What's New in v1.2.6**
> - Added support for latest AI models: GPT-5.2, Opus 4.5, Gemini 3, and xAI's Grok 4.1
>
> **🚀 New Feature: Model Context Protocol (MCP) is now available!**
> You can now connect Smart Composer to external AI tools and using the open MCP standard.
> **What's New**
>
> **v1.2.7** — Connect your Claude or OpenAI account directly (no API key required)
>
> **v1.2.6** — Support for GPT-5.2, Opus 4.5, Gemini 3, and Grok 4.1
>
> **🔌 MCP Support** — Connect Smart Composer to external tools and data sources via the [Model Context Protocol](https://modelcontextprotocol.io)

> [!WARNING]
> **⚠️ Maintenance Notice**
Expand Down Expand Up @@ -83,7 +85,7 @@ MCP lets you use powerful third-party tools and data sources right inside your c

### Additional Features

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

> [!TIP]
> **Looking for a free option?**
Expand Down
63 changes: 42 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"lodash.isequal": "^4.5.0",
"lucide-react": "^0.447.0",
"minimatch": "^10.0.1",
"openai": "^4.91.1",
"openai": "^6.8.1",
"parse5": "^7.1.2",
"path-browserify": "^1.0.1",
"react": "^18.3.1",
Expand Down
5 changes: 3 additions & 2 deletions src/components/chat-view/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export type ChatProps = {

const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
const app = useApp()
const { settings } = useSettings()
const { settings, setSettings } = useSettings()
const { getRAGEngine } = useRAG()
const { getMcpManager } = useMcp()

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

const { providerClient, model } = getChatModelClient({
settings,
modelId: settings.applyModelId,
settings,
setSettings,
})

const updatedFileContent = await applyChangesToFile({
Expand Down
6 changes: 4 additions & 2 deletions src/components/chat-view/useChatStreamManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ export function useChatStreamManager({
const { providerClient, model } = useMemo(() => {
try {
return getChatModelClient({
settings,
modelId: settings.chatModelId,
settings,
setSettings,
})
} catch (error) {
if (error instanceof LLMModelNotFoundException) {
Expand All @@ -76,8 +77,9 @@ export function useChatStreamManager({
),
})
return getChatModelClient({
settings,
modelId: firstChatModel.id,
settings,
setSettings,
})
}
throw error
Expand Down
2 changes: 2 additions & 0 deletions src/components/settings/SettingsTabRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ChatSection } from './sections/ChatSection'
import { EtcSection } from './sections/EtcSection'
import { McpSection } from './sections/McpSection'
import { ModelsSection } from './sections/ModelsSection'
import { PlanConnectionsSection } from './sections/PlanConnectionsSection'
import { ProvidersSection } from './sections/ProvidersSection'
import { RAGSection } from './sections/RAGSection'
import { TemplateSection } from './sections/TemplateSection'
Expand All @@ -34,6 +35,7 @@ export function SettingsTabRoot({ app, plugin }: SettingsTabRootProps) {
cta
/>
</ObsidianSetting>
<PlanConnectionsSection app={app} plugin={plugin} />
<ChatSection />
<ProvidersSection app={app} plugin={plugin} />
<ModelsSection app={app} plugin={plugin} />
Expand Down
172 changes: 172 additions & 0 deletions src/components/settings/modals/ConnectClaudePlanModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { App, Notice } from 'obsidian'
import { useEffect, useState } from 'react'

import { PROVIDER_TYPES_INFO } from '../../../constants'
import {
buildClaudeCodeAuthorizeUrl,
exchangeClaudeCodeForTokens,
generateClaudeCodePkce,
generateClaudeCodeState,
} from '../../../core/llm/claudeCodeAuth'
import SmartComposerPlugin from '../../../main'
import { ObsidianButton } from '../../common/ObsidianButton'
import { ObsidianSetting } from '../../common/ObsidianSetting'
import { ObsidianTextInput } from '../../common/ObsidianTextInput'
import { ReactModal } from '../../common/ReactModal'

type ConnectClaudePlanModalProps = {
plugin: SmartComposerPlugin
onClose: () => void
}

const CLAUDE_PLAN_PROVIDER_ID = PROVIDER_TYPES_INFO['anthropic-plan']
.defaultProviderId as string

export class ConnectClaudePlanModal extends ReactModal<ConnectClaudePlanModalProps> {
constructor(app: App, plugin: SmartComposerPlugin) {
super({
app: app,
Component: ConnectClaudePlanModalComponent,
props: { plugin },
options: {
title: 'Connect Claude subscription',
},
})
}
}

function ConnectClaudePlanModalComponent({
plugin,
onClose,
}: ConnectClaudePlanModalProps) {
const [authorizeUrl, setAuthorizeUrl] = useState('')
const [code, setCode] = useState('')
const [pkceVerifier, setPkceVerifier] = useState('')
const [state, setState] = useState('')
const [isConnecting, setIsConnecting] = useState(false)

const hasAuthData = authorizeUrl.length > 0 && pkceVerifier.length > 0

useEffect(() => {
;(async () => {
try {
const pkce = await generateClaudeCodePkce()
const newState = generateClaudeCodeState()
const url = buildClaudeCodeAuthorizeUrl({ pkce, state: newState })
setPkceVerifier(pkce.verifier)
setState(newState)
setAuthorizeUrl(url)
} catch {
new Notice('Failed to initialize OAuth flow')
}
})()
}, [])

const connect = async () => {
if (isConnecting) return
if (!hasAuthData) {
new Notice('OAuth link is not ready. Try again.')
return
}
if (!code) {
new Notice('Paste the authorization code')
return
}

try {
setIsConnecting(true)

const tokens = await exchangeClaudeCodeForTokens({
code,
pkceVerifier,
state,
})

if (
!plugin.settings.providers.find(
(p) =>
p.type === 'anthropic-plan' && p.id === CLAUDE_PLAN_PROVIDER_ID,
)
) {
throw new Error('Claude Plan provider not found.')
}
await plugin.setSettings({
...plugin.settings,
providers: plugin.settings.providers.map((p) => {
if (p.type === 'anthropic-plan' && p.id === CLAUDE_PLAN_PROVIDER_ID) {
return {
...p,
oauth: {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: Date.now() + (tokens.expires_in ?? 3600) * 1000,
},
}
}
return p
}),
})

new Notice('Claude Plan connected')
onClose()
} catch {
new Notice('OAuth failed. Double-check the code and try again.')
} finally {
setIsConnecting(false)
}
}

return (
<div>
<div className="smtcmp-plan-connect-steps">
<div className="smtcmp-plan-connect-steps-title">How it works</div>
<ol>
<li>Login to Claude in your browser</li>
<li>Copy the code from the redirected URL</li>
<li>Paste it here and click &quot;Connect&quot;</li>
</ol>
</div>

<ObsidianSetting
name="Claude login"
desc="Login to Claude Code in your browser."
>
<ObsidianButton
text="Login to Claude"
disabled={!authorizeUrl || isConnecting}
onClick={() => {
if (!authorizeUrl) return
window.open(authorizeUrl, '_blank')
}}
cta
/>
</ObsidianSetting>

<ObsidianSetting
name="Authorization code"
desc="Paste the code from the redirected URL."
required
>
<ObsidianTextInput
value={code}
placeholder="Paste authorization code"
onChange={(value) => setCode(value)}
/>
</ObsidianSetting>

<ObsidianSetting>
<ObsidianButton
text="Connect"
onClick={() => void connect()}
disabled={isConnecting}
cta
/>
<ObsidianButton
text="Cancel"
onClick={onClose}
disabled={isConnecting}
/>
</ObsidianSetting>
</div>
)
}
Loading