- Streaming chat sidebar with OpenAI-compatible API client - Path-referencing context injection via MCP tools - Dark theme styling with Obsidian CSS variables - Settings: gateway URL, API key, context/model toggles - SSE streaming with fetch timeout protection - Session continuity via X-Hermes-Session-Id headers
8 lines
12 KiB
JavaScript
8 lines
12 KiB
JavaScript
"use strict";var S=Object.defineProperty;var H=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var L=Object.prototype.hasOwnProperty;var M=(o,t)=>{for(var e in t)S(o,e,{get:t[e],enumerable:!0})},N=(o,t,e,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of U(t))!L.call(o,i)&&i!==e&&S(o,i,{get:()=>t[i],enumerable:!(s=H(t,i))||s.enumerable});return o};var j=o=>N(S({},"__esModule",{value:!0}),o);var K={};M(K,{default:()=>x});module.exports=j(K);var D=require("obsidian");var h=require("obsidian"),$={gatewayUrl:"http://192.168.1.12:8642/v1",apiKey:"0f224b188b52e8728d135ec339f8db8dfbf82d89a60021dcfe20d91c6174cf9b",showModelInfo:!0,enableContextInjection:!0},v=class extends h.PluginSettingTab{plugin;constructor(t,e){super(t,e),this.plugin=e}display(){let{containerEl:t}=this;t.empty(),t.createEl("h2",{text:"Hermes Chat Settings"}),new h.Setting(t).setName("Gateway URL").setDesc("Base URL of your Hermes Gateway API server (e.g. http://192.168.1.12:8642/v1)").addText(e=>e.setPlaceholder("http://192.168.1.12:8642/v1").setValue(this.plugin.settings.gatewayUrl).onChange(async s=>{this.plugin.settings.gatewayUrl=s,await this.plugin.saveData(this.plugin.settings)})),new h.Setting(t).setName("API Key").setDesc("API key for authenticating with the Hermes Gateway").addText(e=>e.setPlaceholder("Enter your API key").setValue(this.plugin.settings.apiKey).onChange(async s=>{this.plugin.settings.apiKey=s,await this.plugin.saveData(this.plugin.settings)})),new h.Setting(t).setName("Show model info").setDesc("Display the current model name in the status bar").addToggle(e=>e.setValue(this.plugin.settings.showModelInfo).onChange(async s=>{this.plugin.settings.showModelInfo=s,await this.plugin.saveData(this.plugin.settings)})),new h.Setting(t).setName("Enable context injection").setDesc("Automatically send the active note as context with each message").addToggle(e=>e.setValue(this.plugin.settings.enableContextInjection).onChange(async s=>{this.plugin.settings.enableContextInjection=s,await this.plugin.saveData(this.plugin.settings)}))}};var k=require("obsidian"),d="hermes-chat-view",f=class extends k.ItemView{apiClient=null;contextBridge=null;chatState;messagesContainer=null;statusBar=null;loadingEl=null;textarea=null;sendButton=null;constructor(t,e,s){super(t),this.apiClient=e??null,this.contextBridge=s??null,this.chatState={sessionId:null,messages:[],isLoading:!1,model:"hermes-agent",contextActive:!1}}getViewType(){return d}getDisplayText(){return"Hermes"}async onOpen(){let t=this.containerEl.children[1];t.empty(),t.addClass("hermes-chat-container"),this.messagesContainer=t.createDiv("chat-messages"),this.messagesContainer.id="chat-messages",this.loadingEl=this.messagesContainer.createDiv("chat-loading"),this.loadingEl.setText("Thinking\u2026"),this.loadingEl.style.display="none";let e=t.createDiv("chat-input-area");this.textarea=e.createEl("textarea"),this.textarea.placeholder="Type your message\u2026",this.textarea.setAttribute("rows","1"),this.textarea.addEventListener("keydown",s=>{s.key==="Enter"&&!s.shiftKey&&(s.preventDefault(),this.handleSend())}),this.sendButton=e.createEl("button"),this.sendButton.setText("Send"),this.sendButton.addEventListener("click",()=>this.handleSend()),this.statusBar=t.createDiv("chat-status-bar"),this.updateStatus(this.chatState.model,null)}async onClose(){}setApiClient(t){this.apiClient=t}setContextActive(t){this.chatState.contextActive=t}appendMessage(t,e){if(!this.messagesContainer)return;let s=this.messagesContainer.createDiv("chat-message");s.addClass(`chat-message-${t}`),s.createDiv("chat-message-role").setText(t==="user"?"You":t==="assistant"?"Hermes":"System"),s.createDiv("chat-message-content").setText(e),this.scrollToBottom()}setLoading(t){this.chatState.isLoading=t,this.loadingEl&&(this.loadingEl.style.display=t?"block":"none"),this.textarea&&(this.textarea.disabled=t),this.sendButton&&(this.sendButton.disabled=t)}updateStatus(t,e){if(this.chatState.model=t,this.chatState.sessionId=e,this.statusBar){let s=e?`Session: ${e}`:"Session: \u2014";this.statusBar.setText(`Model: ${t} | ${s}`)}}clearChat(){if(!this.messagesContainer)return;let t=Array.from(this.messagesContainer.children);for(let e of t)e!==this.loadingEl&&e.remove();this.chatState.messages=[]}scrollToBottom(){this.messagesContainer&&(this.messagesContainer.scrollTop=this.messagesContainer.scrollHeight)}async handleSend(){if(!this.textarea)return;let t=this.textarea.value.trim();if(!t)return;if(this.textarea.value="",this.appendMessage("user",t),!this.apiClient){this.appendMessage("assistant","[API client not configured. Check plugin settings.]");return}this.setLoading(!0);let e=this.messagesContainer?.createDiv("chat-message chat-message-assistant"),s=e?.createDiv("chat-message-role");s&&s.setText("Hermes");let i=e?.createDiv("chat-message-content");i&&i.setText("");let n="";try{let a=this.contextBridge?this.contextBridge.buildMessages(t,this.chatState.contextActive):[{role:"user",content:t}],c=await this.apiClient.streamChat(a,l=>{n+=l,i&&i.setText(n),this.scrollToBottom()});c.sessionId&&(this.chatState.sessionId=c.sessionId,this.updateStatus(c.model,c.sessionId))}catch(a){i&&i.setText(`Error: ${a.message||"Unknown error"}`)}finally{this.setLoading(!1)}}};var C=class{baseUrl;apiKey;model;currentSessionId;constructor(t){this.baseUrl=t.baseUrl.replace(/\/+$/,""),this.apiKey=t.apiKey,this.model=t.model,this.currentSessionId=null}updateConfig(t){t.baseUrl!==void 0&&(this.baseUrl=t.baseUrl.replace(/\/+$/,"")),t.apiKey!==void 0&&(this.apiKey=t.apiKey),t.model!==void 0&&(this.model=t.model)}getSessionId(){return this.currentSessionId}get rootUrl(){return this.baseUrl.endsWith("/v1")?this.baseUrl.slice(0,-3):new URL("/",this.baseUrl).href.replace(/\/+$/,"")}async fetchWithTimeout(t,e,s=6e4){let i=new AbortController,n=setTimeout(()=>i.abort(),s);try{return await fetch(t,{...e,signal:i.signal})}finally{clearTimeout(n)}}async extractErrorBody(t){let e="";if((t.headers.get("content-type")??"").includes("application/json"))try{let i=await t.json();if(i.error?.message?e=i.error.message:typeof i.message=="string"?e=i.message:typeof i.error=="string"&&(e=i.error),e)return e}catch{}try{let i=await t.text();if(i.trim())return e=i.trim().substring(0,300),e}catch{}return t.statusText||`HTTP ${t.status}`}async chat(t,e=!1){let s={Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"};this.currentSessionId&&(s["X-Hermes-Session-Id"]=this.currentSessionId);let i=await this.fetchWithTimeout(`${this.baseUrl}/chat/completions`,{method:"POST",headers:s,body:JSON.stringify({model:this.model,messages:t,stream:e})});if(!i.ok){let p=await this.extractErrorBody(i);throw new Error(`API error ${i.status}: ${p}`)}let n=i.headers.get("X-Hermes-Session-Id");n&&(this.currentSessionId=n);let a=await i.json();if(a.error)throw new Error(a.error.message||"Unknown API error");return{content:a.choices?.[0]?.message?.content??"",sessionId:this.currentSessionId??"",model:a.model??this.model,usage:a.usage}}async streamChat(t,e){let s={Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"};this.currentSessionId&&(s["X-Hermes-Session-Id"]=this.currentSessionId);let i=await this.fetchWithTimeout(`${this.baseUrl}/chat/completions`,{method:"POST",headers:s,body:JSON.stringify({model:this.model,messages:t,stream:!0})});if(!i.ok){let u=await this.extractErrorBody(i);throw new Error(`API error ${i.status}: ${u}`)}let n=i.headers.get("X-Hermes-Session-Id");n&&(this.currentSessionId=n);let a=i.body?.getReader();if(!a)throw new Error("Response body is not readable");let c=new TextDecoder("utf-8"),l="",p="",T=this.model,I;for(;;){let{done:u,value:y}=await a.read();if(u)break;l+=c.decode(y,{stream:!0});let g=l.split(`
|
|
|
|
`);l=g.pop()??"";for(let E of g){let m=E.split(`
|
|
`);for(let B of m){if(!B.startsWith("data: "))continue;let A=B.slice(6).trim();if(A!=="[DONE]")try{let r=JSON.parse(A);if(r.error)throw new Error(r.error.message||"Stream error");let b=r.choices?.[0]?.delta?.content;b&&(p+=b,e(b)),r.model&&(T=r.model),r.usage&&(I=r.usage)}catch(r){if(r instanceof Error&&r.message!=="Unexpected token"&&!r.message.includes("JSON"))throw r}}}}if(l.trim()){let u=l.split(`
|
|
`);for(let y of u){if(!y.startsWith("data: "))continue;let g=y.slice(6).trim();if(g!=="[DONE]")try{let m=JSON.parse(g).choices?.[0]?.delta?.content;m&&(p+=m,e(m))}catch{}}}return{content:p,sessionId:this.currentSessionId??"",model:T,usage:I}}async getModels(){let t=await this.fetchWithTimeout(`${this.baseUrl}/models`,{headers:{Authorization:`Bearer ${this.apiKey}`}});if(!t.ok){let s=await this.extractErrorBody(t);throw new Error(`Failed to fetch models: ${t.status} \u2014 ${s}`)}return(await t.json()).data??[]}async getCapabilities(){let t=await this.fetchWithTimeout(`${this.baseUrl}/capabilities`,{headers:{Authorization:`Bearer ${this.apiKey}`}});if(!t.ok){let e=await this.extractErrorBody(t);throw new Error(`Failed to fetch capabilities: ${t.status} \u2014 ${e}`)}return t.json()}async healthCheck(){try{let t=await this.fetchWithTimeout(`${this.rootUrl}/health`,{},1e4);return t.ok?(await t.json()).status==="ok":!1}catch{return!1}}};var P=require("obsidian"),w=class{app;constructor(t){this.app=t}getActiveNoteContext(){let t=this.app.workspace.activeEditor;if(!t||!t.editor)return null;let e=this.app.workspace.activeLeaf;if(!e||!(e.view instanceof P.MarkdownView))return null;let s=e.view.file;return s?{vault:this.app.vault.getName(),path:s.path,title:s.basename}:null}getSelectionContext(){let t=this.app.workspace.activeEditor;return!t||!t.editor?null:t.editor.getSelection()||null}formatContextMessage(t){return{role:"system",content:[`[Current note: ${t.vault}/${t.path}]`,`Use mcp_obsidian_get_vault_document(vault_name="${t.vault}", doc_path="${t.path}") to read the full contents.`].join(`
|
|
`)}}buildMessages(t,e){let s=[];if(e){let i=this.getSelectionContext();if(i){let n=this.getActiveNoteContext(),a=n?`${n.vault}/${n.path}`:"current note";s.push({role:"system",content:`[Selection from ${a}]
|
|
|
|
${i}`})}else{let n=this.getActiveNoteContext();n&&s.push(this.formatContextMessage(n))}}return s.push({role:"user",content:t}),s}};var x=class extends D.Plugin{settings;statusBarItem=null;apiClient=null;contextBridge=null;async onload(){await this.loadSettings(),this.registerView(d,e=>{let s=new f(e,this.apiClient,this.contextBridge);return s.setContextActive(this.settings.enableContextInjection),s}),this.addSettingTab(new v(this.app,this)),this.addRibbonIcon("message-square","Open Hermes Chat",()=>{this.activateView()}),this.settings.showModelInfo&&(this.statusBarItem=this.addStatusBarItem(),this.statusBarItem.setText("Hermes: connecting..."));let t={baseUrl:this.settings.gatewayUrl,apiKey:this.settings.apiKey,model:"hermes-agent"};this.apiClient=new C(t),this.contextBridge=new w(this.app),this.addCommand({id:"open-hermes-chat",name:"Open Hermes Chat",callback:()=>{this.activateView()}}),this.addCommand({id:"send-note-as-context",name:"Send Current Note as Context",callback:()=>{let e=this.contextBridge?.getActiveNoteContext();e?(this.activateView(),console.log(`Context ready: vault=${e.vault}, path=${e.path}`)):console.log("No active note to send as context")}}),this.statusBarItem&&this.apiClient&&(this.statusBarItem.setText("Hermes: checking..."),this.apiClient.healthCheck().then(e=>{this.statusBarItem&&this.statusBarItem.setText(e?"Hermes: connected":"Hermes: unreachable")}))}async onunload(){this.app.workspace.detachLeavesOfType(d)}async activateView(){let{workspace:t}=this.app,e=t.getLeavesOfType(d);if(e.length>0){t.revealLeaf(e[0]);return}let s=t.getRightLeaf(!1);s&&(await s.setViewState({type:d,active:!0}),t.revealLeaf(s))}async loadSettings(){this.settings=Object.assign({},$,await this.loadData())}async saveSettings(){await this.saveData(this.settings),this.syncContextToViews()}syncContextToViews(){let t=this.app.workspace.getLeavesOfType(d);for(let e of t)e.view instanceof f&&e.view.setContextActive(this.settings.enableContextInjection)}};
|