From cbb490002c35ec9a45d6b45d9a3f2d4f7e51ab86 Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:01:46 +0100 Subject: [PATCH] Fix CORS by removing outdated anthropic-beta header The anthropic-beta: messages-2023-12-15 header triggered CORS preflight failures. Removed it (streaming is stable API) and switched non-streaming chat() to use Obsidian's requestUrl which bypasses CORS entirely. Co-Authored-By: Claude Sonnet 4.6 --- main.js | 107 ++++++++++++++++++++++---------------------- src/ClaudeClient.ts | 39 ++++++++-------- 2 files changed, 74 insertions(+), 72 deletions(-) diff --git a/main.js b/main.js index 67bda72..bc3a65e 100644 --- a/main.js +++ b/main.js @@ -19,14 +19,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru // src/main.ts var main_exports = {}; __export(main_exports, { - default: () => VaultChatPlugin + default: () => MemexChatPlugin }); module.exports = __toCommonJS(main_exports); -var import_obsidian3 = require("obsidian"); +var import_obsidian4 = require("obsidian"); // src/ChatView.ts var import_obsidian = require("obsidian"); -var VIEW_TYPE_VAULT_CHAT = "vault-chat-view"; +var VIEW_TYPE_MEMEX_CHAT = "memex-chat-view"; var ChatView = class extends import_obsidian.ItemView { constructor(leaf, plugin) { super(leaf); @@ -39,10 +39,10 @@ var ChatView = class extends import_obsidian.ItemView { this.renderComponent = new import_obsidian.Component(); } getViewType() { - return VIEW_TYPE_VAULT_CHAT; + return VIEW_TYPE_MEMEX_CHAT; } getDisplayText() { - return "Vault Chat"; + return "Memex Chat"; } getIcon() { return "message-circle"; @@ -67,7 +67,7 @@ var ChatView = class extends import_obsidian.ItemView { root.empty(); root.addClass("vc-root"); const header = root.createDiv("vc-header"); - header.createEl("span", { text: "Vault Chat", cls: "vc-header-title" }); + header.createEl("span", { text: "Memex Chat", cls: "vc-header-title" }); const headerActions = header.createDiv("vc-header-actions"); const newThreadBtn = headerActions.createEl("button", { cls: "vc-icon-btn", title: "Neuer Thread" }); newThreadBtn.innerHTML = ``; @@ -645,21 +645,24 @@ var VaultSearch = class { }; // src/ClaudeClient.ts +var import_obsidian2 = require("obsidian"); var ClaudeClient = class { constructor() { this.baseUrl = "https://api.anthropic.com/v1/messages"; } + headers(apiKey) { + return { + "content-type": "application/json", + "x-api-key": apiKey, + "anthropic-version": "2023-06-01" + }; + } /** Stream a chat completion, yielding text chunks */ async *streamChat(messages, options) { var _a, _b, _c, _d, _e, _f; const response = await fetch(this.baseUrl, { method: "POST", - headers: { - "content-type": "application/json", - "x-api-key": options.apiKey, - "anthropic-version": "2023-06-01", - "anthropic-beta": "messages-2023-12-15" - }, + headers: this.headers(options.apiKey), body: JSON.stringify({ model: options.model, max_tokens: (_a = options.maxTokens) != null ? _a : 2048, @@ -712,34 +715,30 @@ var ClaudeClient = class { } yield { type: "done" }; } - /** Non-streaming version for simpler use cases */ + /** Non-streaming version — uses Obsidian's requestUrl to bypass CORS */ async chat(messages, options) { var _a, _b, _c, _d; - const response = await fetch(this.baseUrl, { + const response = await (0, import_obsidian2.requestUrl)({ + url: this.baseUrl, method: "POST", - headers: { - "content-type": "application/json", - "x-api-key": options.apiKey, - "anthropic-version": "2023-06-01" - }, + headers: this.headers(options.apiKey), body: JSON.stringify({ model: options.model, max_tokens: (_a = options.maxTokens) != null ? _a : 2048, system: options.systemPrompt, messages - }) + }), + throw: false }); - if (!response.ok) { - const err = await response.text(); - throw new Error(`API Error ${response.status}: ${err}`); + if (response.status >= 400) { + throw new Error(`API Error ${response.status}: ${response.text}`); } - const data = await response.json(); - return (_d = (_c = (_b = data.content) == null ? void 0 : _b[0]) == null ? void 0 : _c.text) != null ? _d : ""; + return (_d = (_c = (_b = response.json.content) == null ? void 0 : _b[0]) == null ? void 0 : _c.text) != null ? _d : ""; } }; // src/SettingsTab.ts -var import_obsidian2 = require("obsidian"); +var import_obsidian3 = require("obsidian"); var DEFAULT_SETTINGS = { apiKey: "", model: "claude-opus-4-5-20251101", @@ -764,7 +763,7 @@ var MODELS = [ { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5 (Schnell)" }, { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5" } ]; -var VaultChatSettingsTab = class extends import_obsidian2.PluginSettingTab { +var MemexChatSettingsTab = class extends import_obsidian3.PluginSettingTab { constructor(app, plugin) { super(app, plugin); this.plugin = plugin; @@ -772,15 +771,15 @@ var VaultChatSettingsTab = class extends import_obsidian2.PluginSettingTab { display() { const { containerEl } = this; containerEl.empty(); - containerEl.createEl("h2", { text: "Vault Chat Einstellungen" }); + containerEl.createEl("h2", { text: "Memex Chat Einstellungen" }); containerEl.createEl("h3", { text: "Claude API" }); - new import_obsidian2.Setting(containerEl).setName("API Key").setDesc("Dein Anthropic API Key (sk-ant-...)").addText( + new import_obsidian3.Setting(containerEl).setName("API Key").setDesc("Dein Anthropic API Key (sk-ant-...)").addText( (text) => text.setPlaceholder("sk-ant-api03-...").setValue(this.plugin.settings.apiKey).onChange(async (value) => { this.plugin.settings.apiKey = value.trim(); await this.plugin.saveSettings(); }) ); - new import_obsidian2.Setting(containerEl).setName("Modell").setDesc("Welches Claude-Modell verwenden?").addDropdown((drop) => { + new import_obsidian3.Setting(containerEl).setName("Modell").setDesc("Welches Claude-Modell verwenden?").addDropdown((drop) => { for (const m of MODELS) drop.addOption(m.id, m.name); drop.setValue(this.plugin.settings.model).onChange(async (value) => { @@ -789,45 +788,45 @@ var VaultChatSettingsTab = class extends import_obsidian2.PluginSettingTab { }); }); containerEl.createEl("h3", { text: "Kontext-Einstellungen" }); - new import_obsidian2.Setting(containerEl).setName("Max. Kontext-Notizen").setDesc("Wie viele Notizen werden automatisch als Kontext hinzugef\xFCgt? (1\u201315)").addSlider( + new import_obsidian3.Setting(containerEl).setName("Max. Kontext-Notizen").setDesc("Wie viele Notizen werden automatisch als Kontext hinzugef\xFCgt? (1\u201315)").addSlider( (slider) => slider.setLimits(1, 15, 1).setValue(this.plugin.settings.maxContextNotes).setDynamicTooltip().onChange(async (value) => { this.plugin.settings.maxContextNotes = value; await this.plugin.saveSettings(); }) ); - new import_obsidian2.Setting(containerEl).setName("Max. Zeichen pro Notiz").setDesc("Wie viele Zeichen einer Notiz in den Kontext einbezogen werden (1000\u20138000)").addSlider( + new import_obsidian3.Setting(containerEl).setName("Max. Zeichen pro Notiz").setDesc("Wie viele Zeichen einer Notiz in den Kontext einbezogen werden (1000\u20138000)").addSlider( (slider) => slider.setLimits(1e3, 8e3, 500).setValue(this.plugin.settings.maxCharsPerNote).setDynamicTooltip().onChange(async (value) => { this.plugin.settings.maxCharsPerNote = value; await this.plugin.saveSettings(); }) ); - new import_obsidian2.Setting(containerEl).setName("Automatischer Kontext-Abruf").setDesc("Beim Senden automatisch relevante Notizen suchen und einbinden").addToggle( + new import_obsidian3.Setting(containerEl).setName("Automatischer Kontext-Abruf").setDesc("Beim Senden automatisch relevante Notizen suchen und einbinden").addToggle( (toggle) => toggle.setValue(this.plugin.settings.autoRetrieveContext).onChange(async (value) => { this.plugin.settings.autoRetrieveContext = value; await this.plugin.saveSettings(); }) ); - new import_obsidian2.Setting(containerEl).setName("Kontext-Vorschau anzeigen").setDesc("Vor dem Senden zeigen, welche Notizen als Kontext verwendet werden").addToggle( + new import_obsidian3.Setting(containerEl).setName("Kontext-Vorschau anzeigen").setDesc("Vor dem Senden zeigen, welche Notizen als Kontext verwendet werden").addToggle( (toggle) => toggle.setValue(this.plugin.settings.showContextPreview).onChange(async (value) => { this.plugin.settings.showContextPreview = value; await this.plugin.saveSettings(); }) ); containerEl.createEl("h3", { text: "Thread-History" }); - new import_obsidian2.Setting(containerEl).setName("Threads im Vault speichern").setDesc("Chat-Threads als Markdown-Notizen im Vault ablegen").addToggle( + new import_obsidian3.Setting(containerEl).setName("Threads im Vault speichern").setDesc("Chat-Threads als Markdown-Notizen im Vault ablegen").addToggle( (toggle) => toggle.setValue(this.plugin.settings.saveThreadsToVault).onChange(async (value) => { this.plugin.settings.saveThreadsToVault = value; await this.plugin.saveSettings(); }) ); - new import_obsidian2.Setting(containerEl).setName("Threads-Ordner").setDesc("Pfad im Vault, wo Chat-Threads gespeichert werden").addText( + new import_obsidian3.Setting(containerEl).setName("Threads-Ordner").setDesc("Pfad im Vault, wo Chat-Threads gespeichert werden").addText( (text) => text.setPlaceholder("Calendar/Chat").setValue(this.plugin.settings.threadsFolder).onChange(async (value) => { this.plugin.settings.threadsFolder = value; await this.plugin.saveSettings(); }) ); containerEl.createEl("h3", { text: "System Prompt" }); - new import_obsidian2.Setting(containerEl).setName("System Prompt").setDesc("Instruktionen f\xFCr Claude (wie soll er sich verhalten?)").addTextArea((textarea) => { + new import_obsidian3.Setting(containerEl).setName("System Prompt").setDesc("Instruktionen f\xFCr Claude (wie soll er sich verhalten?)").addTextArea((textarea) => { textarea.setValue(this.plugin.settings.systemPrompt).onChange(async (value) => { this.plugin.settings.systemPrompt = value; await this.plugin.saveSettings(); @@ -838,7 +837,7 @@ var VaultChatSettingsTab = class extends import_obsidian2.PluginSettingTab { textarea.inputEl.style.fontSize = "12px"; }); containerEl.createEl("h3", { text: "Aktionen" }); - new import_obsidian2.Setting(containerEl).setName("Index neu aufbauen").setDesc("Vault-Index f\xFCr die Suche neu aufbauen (dauert je nach Vault-Gr\xF6\xDFe einige Sekunden)").addButton( + new import_obsidian3.Setting(containerEl).setName("Index neu aufbauen").setDesc("Vault-Index f\xFCr die Suche neu aufbauen (dauert je nach Vault-Gr\xF6\xDFe einige Sekunden)").addButton( (btn) => btn.setButtonText("Index neu aufbauen").setCta().onClick(async () => { btn.setButtonText("Indiziere\u2026"); btn.setDisabled(true); @@ -854,7 +853,7 @@ var VaultChatSettingsTab = class extends import_obsidian2.PluginSettingTab { }; // src/main.ts -var VaultChatPlugin = class extends import_obsidian3.Plugin { +var MemexChatPlugin = class extends import_obsidian4.Plugin { async onload() { var _a, _b; const loaded = await this.loadData(); @@ -865,28 +864,28 @@ var VaultChatPlugin = class extends import_obsidian3.Plugin { this.settings = this.data.settings; this.search = new VaultSearch(this.app); this.claude = new ClaudeClient(); - this.registerView(VIEW_TYPE_VAULT_CHAT, (leaf) => new ChatView(leaf, this)); - this.addRibbonIcon("message-circle", "Vault Chat \xF6ffnen", () => { + this.registerView(VIEW_TYPE_MEMEX_CHAT, (leaf) => new ChatView(leaf, this)); + this.addRibbonIcon("message-circle", "Memex Chat \xF6ffnen", () => { this.activateView(); }); this.addCommand({ - id: "open-vault-chat", - name: "Vault Chat \xF6ffnen", + id: "open-memex-chat", + name: "Memex Chat \xF6ffnen", callback: () => this.activateView() }); this.addCommand({ - id: "vault-chat-rebuild-index", - name: "Vault Chat: Index neu aufbauen", + id: "memex-chat-rebuild-index", + name: "Memex Chat: Index neu aufbauen", callback: () => this.rebuildIndex() }); this.addCommand({ - id: "vault-chat-active-note", - name: "Vault Chat: Aktive Notiz als Kontext", + id: "memex-chat-active-note", + name: "Memex Chat: Aktive Notiz als Kontext", callback: () => { const file = this.app.workspace.getActiveFile(); if (file) { this.activateView().then(() => { - const leaf = this.app.workspace.getLeavesOfType(VIEW_TYPE_VAULT_CHAT)[0]; + const leaf = this.app.workspace.getLeavesOfType(VIEW_TYPE_MEMEX_CHAT)[0]; if (leaf) { const view = leaf.view; view.inputEl.value = `Erkl\xE4re und verkn\xFCpfe [[${file.basename}]] mit anderen Konzepten im Vault.`; @@ -896,19 +895,19 @@ var VaultChatPlugin = class extends import_obsidian3.Plugin { } } }); - this.addSettingTab(new VaultChatSettingsTab(this.app, this)); + this.addSettingTab(new MemexChatSettingsTab(this.app, this)); setTimeout(() => { if (!this.search.isIndexed()) { this.search.buildIndex().catch(console.error); } }, 3e3); - console.log("[Vault Chat] Plugin geladen"); + console.log("[Memex Chat] Plugin geladen"); } onunload() { - this.app.workspace.detachLeavesOfType(VIEW_TYPE_VAULT_CHAT); + this.app.workspace.detachLeavesOfType(VIEW_TYPE_MEMEX_CHAT); } async activateView() { - const existing = this.app.workspace.getLeavesOfType(VIEW_TYPE_VAULT_CHAT); + const existing = this.app.workspace.getLeavesOfType(VIEW_TYPE_MEMEX_CHAT); if (existing.length > 0) { this.app.workspace.revealLeaf(existing[0]); return; @@ -916,12 +915,12 @@ var VaultChatPlugin = class extends import_obsidian3.Plugin { const leaf = this.app.workspace.getRightLeaf(false); if (!leaf) return; - await leaf.setViewState({ type: VIEW_TYPE_VAULT_CHAT, active: true }); + await leaf.setViewState({ type: VIEW_TYPE_MEMEX_CHAT, active: true }); this.app.workspace.revealLeaf(leaf); } async rebuildIndex() { var _a; - const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_VAULT_CHAT); + const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_MEMEX_CHAT); const view = (_a = leaves[0]) == null ? void 0 : _a.view; this.search.onProgress = (done, total) => { if (view && done % 200 === 0) { diff --git a/src/ClaudeClient.ts b/src/ClaudeClient.ts index daa46aa..2474e26 100644 --- a/src/ClaudeClient.ts +++ b/src/ClaudeClient.ts @@ -1,3 +1,5 @@ +import { requestUrl } from "obsidian"; + export interface ClaudeMessage { role: "user" | "assistant"; content: string; @@ -20,19 +22,24 @@ export interface ClaudeStreamChunk { export class ClaudeClient { private baseUrl = "https://api.anthropic.com/v1/messages"; + private headers(apiKey: string): Record { + return { + "content-type": "application/json", + "x-api-key": apiKey, + "anthropic-version": "2023-06-01", + }; + } + /** Stream a chat completion, yielding text chunks */ async *streamChat( messages: ClaudeMessage[], options: ClaudeOptions ): AsyncGenerator { + // Use native fetch for streaming (requestUrl doesn't support streaming). + // The outdated anthropic-beta header is omitted — streaming is stable API. const response = await fetch(this.baseUrl, { method: "POST", - headers: { - "content-type": "application/json", - "x-api-key": options.apiKey, - "anthropic-version": "2023-06-01", - "anthropic-beta": "messages-2023-12-15", - }, + headers: this.headers(options.apiKey), body: JSON.stringify({ model: options.model, max_tokens: options.maxTokens ?? 2048, @@ -90,29 +97,25 @@ export class ClaudeClient { yield { type: "done" }; } - /** Non-streaming version for simpler use cases */ + /** Non-streaming version — uses Obsidian's requestUrl to bypass CORS */ async chat(messages: ClaudeMessage[], options: ClaudeOptions): Promise { - const response = await fetch(this.baseUrl, { + const response = await requestUrl({ + url: this.baseUrl, method: "POST", - headers: { - "content-type": "application/json", - "x-api-key": options.apiKey, - "anthropic-version": "2023-06-01", - }, + headers: this.headers(options.apiKey), body: JSON.stringify({ model: options.model, max_tokens: options.maxTokens ?? 2048, system: options.systemPrompt, messages, }), + throw: false, }); - if (!response.ok) { - const err = await response.text(); - throw new Error(`API Error ${response.status}: ${err}`); + if (response.status >= 400) { + throw new Error(`API Error ${response.status}: ${response.text}`); } - const data = await response.json(); - return data.content?.[0]?.text ?? ""; + return response.json.content?.[0]?.text ?? ""; } }