obsidian-hermes-plugin/src/main.ts
Helm 3b35f59ec8 Initial commit: Obsidian Hermes Chat plugin
- 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
2026-07-10 11:18:23 -04:00

127 lines
3.8 KiB
TypeScript

import { Plugin, WorkspaceLeaf } from "obsidian";
import { HermesChatSettingTab, HermesChatSettings, DEFAULT_SETTINGS } from "./settings";
import { ChatView, VIEW_TYPE } from "./chat-view";
import { ApiClient } from "./api-client";
import { ContextBridge } from "./context-bridge";
import type { GatewayConfig } from "./types";
export default class HermesChatPlugin extends Plugin {
settings: HermesChatSettings;
private statusBarItem: HTMLElement | null = null;
private apiClient: ApiClient | null = null;
private contextBridge: ContextBridge | null = null;
async onload() {
await this.loadSettings();
// Register the chat view
this.registerView(
VIEW_TYPE,
(leaf: WorkspaceLeaf) => {
const view = new ChatView(leaf, this.apiClient!, this.contextBridge!);
view.setContextActive(this.settings.enableContextInjection);
return view;
}
);
// Add settings tab
this.addSettingTab(new HermesChatSettingTab(this.app, this));
// Ribbon icon to activate chat view
this.addRibbonIcon("message-square", "Open Hermes Chat", () => {
this.activateView();
});
// Status bar item showing model name
if (this.settings.showModelInfo) {
this.statusBarItem = this.addStatusBarItem();
this.statusBarItem.setText("Hermes: connecting...");
}
// Initialize API client from settings
const gatewayConfig: GatewayConfig = {
baseUrl: this.settings.gatewayUrl,
apiKey: this.settings.apiKey,
model: "hermes-agent",
};
this.apiClient = new ApiClient(gatewayConfig);
this.contextBridge = new ContextBridge(this.app);
// Command: Open Hermes Chat
this.addCommand({
id: "open-hermes-chat",
name: "Open Hermes Chat",
callback: () => {
this.activateView();
},
});
// Command: Send Current Note as Context
this.addCommand({
id: "send-note-as-context",
name: "Send Current Note as Context",
callback: () => {
const context = this.contextBridge?.getActiveNoteContext();
if (context) {
this.activateView();
// Note: actual context injection happens in chat-view's handleSend via the contextBridge
console.log(`Context ready: vault=${context.vault}, path=${context.path}`);
} else {
console.log("No active note to send as context");
}
},
});
// Check gateway connectivity
if (this.statusBarItem && this.apiClient) {
this.statusBarItem.setText("Hermes: checking...");
this.apiClient.healthCheck().then((healthy) => {
if (this.statusBarItem) {
this.statusBarItem.setText(healthy ? "Hermes: connected" : "Hermes: unreachable");
}
});
}
}
async onunload() {
// Detach the view when plugin is unloaded
this.app.workspace.detachLeavesOfType(VIEW_TYPE);
}
async activateView() {
const { workspace } = this.app;
// Check if view already exists
const existing = workspace.getLeavesOfType(VIEW_TYPE);
if (existing.length > 0) {
workspace.revealLeaf(existing[0]);
return;
}
// Create new leaf in right sidebar
const leaf = workspace.getRightLeaf(false);
if (leaf) {
await leaf.setViewState({ type: VIEW_TYPE, active: true });
workspace.revealLeaf(leaf);
}
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
this.syncContextToViews();
}
/** Push enableContextInjection setting to any open ChatViews. */
private syncContextToViews(): void {
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE);
for (const leaf of leaves) {
if (leaf.view instanceof ChatView) {
leaf.view.setContextActive(this.settings.enableContextInjection);
}
}
}
}