chore: move compiled output to dist

Update the plugin entrypoint and TypeScript output directory to use dist instead of writing generated JavaScript into src.

Remove previously checked-in compiled source files and add coverage for the Ollama client and tool executor.
This commit is contained in:
2026-05-19 17:30:05 +02:00
parent 2e26c72c0c
commit 2d78882594
21 changed files with 2658 additions and 2357 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "ollama-plugin", "name": "ollama-plugin",
"version": "1.0.0", "version": "1.0.0",
"description": "Ollama integration plugin for Obsidian", "description": "Ollama integration plugin for Obsidian",
"main": "main.ts", "main": "dist/main.js",
"scripts": { "scripts": {
"test": "jest", "test": "jest",
"build": "tsc", "build": "tsc",
-2
View File
@@ -1,2 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
-457
View File
@@ -1,457 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChatView = exports.VIEW_TYPE_OLLAMA_CHAT = void 0;
const obsidian_1 = require("obsidian");
const ollama_client_1 = require("./ollama-client");
const vault_indexer_1 = require("./vault-indexer");
const tool_executor_1 = require("./tool-executor");
const conversation_state_1 = require("./conversation-state");
const error_handler_1 = require("./error-handler");
exports.VIEW_TYPE_OLLAMA_CHAT = 'ollama-chat-view';
class ChatView extends obsidian_1.ItemView {
// Getters for testing
getSendButtonClickHandler() {
return this.sendButtonClickHandler;
}
getInputKeyDownHandler() {
return this.inputKeyDownHandler;
}
getNewChatButtonClickHandler() {
return this.newChatButtonClickHandler;
}
constructor(leaf, settings) {
super(leaf);
// State
this.messages = [];
this.lastMessageEl = null;
this.newChatButton = null;
this.sendButton = null;
this.inputEl = null;
this.chatContainer = null;
this.sendButtonClickHandler = null;
this.inputKeyDownHandler = null;
this.newChatButtonClickHandler = null;
this.sendButtonClickWrapper = null;
this.inputKeyDownWrapper = null;
this.newChatButtonClickWrapper = null;
this.listenersAttached = false;
this.messages = [];
this.lastMessageEl = null;
this.newChatButton = null;
this.sendButton = null;
this.inputEl = null;
this.chatContainer = null;
this.sendButtonClickHandler = null;
this.inputKeyDownHandler = null;
this.newChatButtonClickHandler = null;
this.sendButtonClickWrapper = null;
this.inputKeyDownWrapper = null;
this.newChatButtonClickWrapper = null;
this.listenersAttached = false;
this.settings = settings;
this.ollamaClient = new ollama_client_1.OllamaClient(settings.ollamaUrl, settings.model, undefined, settings.cacheConfig);
this.vaultIndexer = new vault_indexer_1.VaultIndexer(this.app.vault);
this.toolExecutor = new tool_executor_1.ToolExecutor(this.app.vault, this.app);
this.conversationStateManager = new conversation_state_1.ConversationStateManager();
}
updateSettings(newSettings) {
this.settings = newSettings;
this.ollamaClient = new ollama_client_1.OllamaClient(newSettings.ollamaUrl, newSettings.model, undefined, newSettings.cacheConfig);
void this.ollamaClient.initializeCache().catch(() => {
new obsidian_1.Notice('Semantic cache unavailable: could not connect to ChromaDB. Check the ChromaDB URL in settings.');
});
}
async clearCache() {
await this.ollamaClient.clearCache();
}
getViewType() {
return 'ollama-chat-view';
}
getDisplayText() {
return 'Ollama Chat';
}
async onOpen() {
try {
await this.ollamaClient.initializeCache();
}
catch {
new obsidian_1.Notice('Semantic cache unavailable: could not connect to ChromaDB. Check the ChromaDB URL in settings.');
}
this.render();
this.removeEventListeners(); // Clean up any existing listeners before reattaching
this.setupEventListeners();
}
onSettingsChange(newSettings) {
this.updateSettings(newSettings);
}
async onClose() {
this.ollamaClient.cancelStream();
this.removeEventListeners();
this.cleanupStreamingResources();
this.lastMessageEl = null;
this.sendButton = null;
this.inputEl = null;
this.chatContainer = null;
return Promise.resolve();
}
cleanupStreamingResources() {
// Only cleanup if there's still an active streaming message
const streamingMessage = this.messages.find((msg) => msg.isStreaming);
if (streamingMessage && this.lastMessageEl && this.lastMessageEl.parentElement) {
this.lastMessageEl.parentElement.removeChild(this.lastMessageEl);
this.lastMessageEl = null;
}
}
render() {
const container = this.chatContainer || this.contentEl.createEl('div', { cls: 'ollama-chat-container' });
this.chatContainer = container;
const inputContainer = this.contentEl.querySelector('.ollama-input-container') ||
this.contentEl.createEl('div', { cls: 'ollama-input-container' });
const newChatContainer = this.contentEl.querySelector('.ollama-new-chat-container') ||
this.contentEl.createEl('div', { cls: 'ollama-new-chat-container' });
const messagesSnapshot = [...this.messages];
const nonStreamingMessages = messagesSnapshot.filter((msg) => !msg.isStreaming);
const existingMessages = container.querySelectorAll('.ollama-message');
// Remove messages that are no longer in the array
for (const el of Array.from(existingMessages)) {
const id = el.getAttribute('data-msg-id');
if (!id || !nonStreamingMessages.some((m) => m.id === id)) {
el.remove();
}
}
// Render non-streaming messages
for (const msg of nonStreamingMessages) {
const existingEl = container.querySelector(`.ollama-message[data-msg-id="${msg.id}"]`);
if (existingEl) {
const contentEl = existingEl.querySelector('.ollama-message-content');
if (contentEl) {
contentEl.textContent = msg.content;
}
}
else {
const messageEl = container.createEl('div', { cls: 'ollama-message' });
messageEl.setAttribute('data-msg-id', msg.id);
messageEl.createEl('div', { cls: 'ollama-message-role', text: msg.role });
const contentEl = messageEl.createEl('div', { cls: 'ollama-message-content' });
contentEl.textContent = msg.content;
}
}
// Re-attach streaming message if it exists
const streamingMessage = messagesSnapshot.find((msg) => msg.isStreaming);
if (streamingMessage && this.lastMessageEl) {
const existingStreamingEl = container.querySelector(`.ollama-message[data-msg-id="${streamingMessage.id}"]`);
if (!existingStreamingEl) {
container.appendChild(this.lastMessageEl);
}
}
// Setup new chat button
if (!this.newChatButton) {
this.newChatButton = newChatContainer.createEl('button', {
cls: 'ollama-new-chat-button',
text: 'New Chat',
});
}
else {
newChatContainer.appendChild(this.newChatButton);
}
// Setup input area
if (!this.inputEl) {
this.inputEl = inputContainer.createEl('textarea', {
cls: 'ollama-input',
attr: { placeholder: 'Type your message...' },
});
}
else {
inputContainer.appendChild(this.inputEl);
}
// Setup send button
if (!this.sendButton) {
this.sendButton = inputContainer.createEl('button', {
cls: 'ollama-send-button',
text: 'Send',
});
}
else {
inputContainer.appendChild(this.sendButton);
}
// Append containers to contentEl
this.contentEl.appendChild(newChatContainer);
this.contentEl.appendChild(inputContainer);
this.contentEl.appendChild(container);
// Focus input on open
this.inputEl.focus();
}
setupEventListeners() {
if (this.listenersAttached) {
return;
}
this.sendButtonClickHandler = () => {
void this.handleUserInput(this.inputEl?.value);
};
this.inputKeyDownHandler = (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
void this.handleUserInput(this.inputEl?.value);
}
};
this.newChatButtonClickHandler = () => {
this.clearConversation();
};
if (this.sendButton && this.sendButtonClickHandler) {
this.sendButton.addEventListener('click', this.sendButtonClickHandler);
}
if (this.inputEl && this.inputKeyDownHandler) {
this.inputEl.addEventListener('keydown', this.inputKeyDownHandler);
}
if (this.newChatButton && this.newChatButtonClickHandler) {
this.newChatButton.addEventListener('click', this.newChatButtonClickHandler);
}
this.listenersAttached = true;
}
removeEventListeners() {
if (!this.listenersAttached) {
return;
}
if (this.sendButton && this.sendButtonClickHandler) {
this.sendButton.removeEventListener('click', this.sendButtonClickHandler);
}
if (this.inputEl && this.inputKeyDownHandler) {
this.inputEl.removeEventListener('keydown', this.inputKeyDownHandler);
}
if (this.newChatButton && this.newChatButtonClickHandler) {
this.newChatButton.removeEventListener('click', this.newChatButtonClickHandler);
}
this.listenersAttached = false;
}
clearConversation() {
this.messages = [];
this.conversationStateManager.clear();
this.render();
}
updateMessageById(id, updates) {
const index = this.messages.findIndex((m) => m.id === id);
if (index !== -1) {
this.messages[index] = { ...this.messages[index], ...updates };
this.render();
}
}
updateLastMessage(updates) {
const streamingMessage = this.messages.find((msg) => msg.isStreaming);
if (streamingMessage) {
const index = this.messages.findIndex((msg) => msg.id === streamingMessage.id);
if (index !== -1) {
this.messages[index] = { ...this.messages[index], ...updates };
this.render();
}
}
}
getTools() {
return [
{
type: 'function',
function: {
name: 'read_vault_file',
description: 'Reads the content of a file from the vault',
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'The path to the file to read',
},
content: {
type: 'string',
description: 'The content of the file to read',
},
},
required: ['path'],
},
},
},
{
type: 'function',
function: {
name: 'search_vault_files',
description: 'Searches for files in the vault that match a given query',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The search query to use',
},
limit: {
type: 'number',
description: 'The maximum number of results to return',
},
},
required: ['query'],
},
},
},
];
}
buildMessages(userMessageContent, tools) {
const systemContent = `You are an assistant that can help answer questions using the contents of a vault.
The user can ask questions about their vault contents, and you should provide helpful responses based on the files.
When a user asks for information, try to find relevant files using the search_vault_files tool and read their contents with the read_vault_file tool.
Only use the tools if you need to access vault content that is not already in the context.`;
const systemMessage = {
role: 'system',
content: systemContent,
};
const userMessage = {
role: 'user',
content: userMessageContent,
};
const messages = [systemMessage, userMessage];
if (tools && tools.length > 0) {
messages.push({
role: 'assistant',
content: 'I have access to the following tools to help answer your questions:',
});
}
return messages;
}
async processToolCalls(toolCalls, messages, tools, fullResponse, assistantMessageId) {
const toolResults = (await Promise.all(toolCalls.slice(0, MAX_TOOL_CALLS).map(async (toolCall) => {
try {
const toolResult = await this.toolExecutor.handleToolCall(toolCall);
return { ...toolResult, id: toolCall.id };
}
catch (error) {
error_handler_1.ErrorHandler.handleError(error, 'ChatView.handleUserInput');
return null;
}
}))).filter((result) => result !== null);
const followUpMessages = toolResults.map((result) => {
return {
role: 'tool',
content: JSON.stringify(result),
tool_call_id: result.id ?? '',
};
});
const followUp = {
role: 'assistant',
content: 'I have processed your request using the following tools. Here are the results:',
tool_calls: toolCalls,
};
if (followUpMessages.length > 0) {
const finalMessages = [...messages, followUp, ...followUpMessages];
const response = await this.ollamaClient.chat(finalMessages, tools);
const finalResponse = response.content || fullResponse;
this.updateMessageById(assistantMessageId, {
content: finalResponse,
isStreaming: false,
});
}
}
async handleUserInput(inputValue) {
const userMessage = (inputValue ?? this.inputEl?.value ?? '').trim();
if (!userMessage) {
return;
}
const MAX_CONTEXT_LENGTH = 2000;
const tools = this.getTools();
const messageId = crypto.randomUUID();
const userMessageId = `${messageId}-user`;
const assistantMessageId = `${messageId}-assistant`;
const userChatMessage = {
id: userMessageId,
role: 'user',
content: userMessage,
timestamp: Date.now(),
};
const assistantMessage = {
id: assistantMessageId,
role: 'assistant',
content: '',
timestamp: Date.now(),
isStreaming: true,
};
const previousStreamingEl = this.lastMessageEl;
this.messages = [...this.messages, userChatMessage, assistantMessage];
this.render();
if (this.inputEl) {
this.inputEl.value = '';
}
// Add the assistant message to the DOM to enable streaming
this.lastMessageEl =
this.chatContainer?.querySelector(`.ollama-message[data-msg-id="${assistantMessageId}"]`) ??
null;
if (!this.lastMessageEl && previousStreamingEl) {
previousStreamingEl.classList.add('ollama-message');
previousStreamingEl.setAttribute('data-msg-id', assistantMessageId);
this.contentEl.appendChild(previousStreamingEl);
this.lastMessageEl = previousStreamingEl;
}
try {
const entries = await this.vaultIndexer.searchVault(userMessage, this.settings.vaultSearchLimit);
const context = entries
.map((entry) => `${entry.title}\n${entry.content}`)
.join('\n\n')
.slice(0, MAX_CONTEXT_LENGTH);
const userMessageWithContext = context
? `Relevant vault context:\n${context}\n\nUser question:\n${userMessage}`
: userMessage;
// Get the complete messages array for the LLM with all context layers
const completeMessages = this.conversationStateManager.getCompleteMessages(userMessageWithContext);
const stream = this.ollamaClient.streamChat(completeMessages, tools);
let fullResponse = '';
let toolCalls = [];
let chunkCount = 0;
for await (const chunk of stream) {
if (chunk.content) {
fullResponse += chunk.content;
this.updateLastMessage({
content: fullResponse,
isStreaming: true,
});
}
if (chunk.tool_calls) {
toolCalls = [...toolCalls, ...chunk.tool_calls];
}
chunkCount++;
if (chunkCount > MAX_STREAM_CHUNKS) {
break;
}
}
// Process tool calls if any
if (toolCalls.length > 0) {
await this.processToolCalls(toolCalls, completeMessages, tools, fullResponse, assistantMessageId);
}
// Update assistant message immutably — only if no tool calls were processed
if (toolCalls.length === 0) {
this.updateMessageById(assistantMessageId, {
content: fullResponse,
isStreaming: false,
});
}
// Update conversation state with the assistant's response
this.conversationStateManager.updateShortTermContext({ role: 'user', content: userMessage });
this.conversationStateManager.updateShortTermContext({
role: 'assistant',
content: fullResponse,
});
// Limit conversation history to prevent memory issues
if (this.messages.length > this.settings.maxMessageHistory) {
this.messages = this.messages.slice(-this.settings.maxMessageHistory);
}
this.render();
}
catch (error) {
error_handler_1.ErrorHandler.handleError(error, 'ChatView.handleUserInput');
this.updateMessageById(assistantMessageId, {
content: 'An error occurred while processing your request.',
isStreaming: false,
});
}
finally {
// Clean up streaming resources regardless of outcome
this.cleanupStreamingResources();
}
}
}
exports.ChatView = ChatView;
const MAX_STREAM_CHUNKS = 1000;
const MAX_TOOL_CALLS = 5;
-17
View File
@@ -1,17 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_SETTINGS = void 0;
exports.DEFAULT_SETTINGS = {
ollamaUrl: 'http://localhost:11434',
model: 'llama3',
vaultSearchLimit: 3,
maxMessageHistory: 50,
lastIndexTime: 0,
cacheConfig: {
enabled: false,
similarityThreshold: 0.85,
collectionName: 'ollama_semantic_cache',
embeddingModel: 'nomic-embed-text',
chromaURL: 'http://localhost:8000',
},
};
-145
View File
@@ -1,145 +0,0 @@
"use strict";
// src/conversation-state.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConversationStateManager = void 0;
class ConversationStateManager {
constructor() {
this.shortTermContext = [];
this.mediumTermContext = [];
this.longTermContext = [];
this.maxShortTermTurns = 10;
this.maxMediumTermMessages = 20;
// Initialize with default system context
this.longTermContext = [
{
role: 'system',
content: `You are an assistant that can help answer questions using the contents of a vault.
The user can ask questions about their vault contents, and you should provide helpful responses based on the files.
When a user asks for information, try to find relevant files using the search_vault_files tool and read their contents with the read_vault_file tool.
Only use the tools if you need to access vault content that is not already in the context.`,
},
];
}
/**
* Updates the short-term context with a new message
* @param message The message to add to short-term context
*/
updateShortTermContext(message) {
// Add new message
this.shortTermContext.push(message);
// Limit to max turns
if (this.shortTermContext.length > this.maxShortTermTurns) {
this.shortTermContext = this.shortTermContext.slice(-this.maxShortTermTurns);
}
}
/**
* Updates the medium-term context with a new message
* @param message The message to add to medium-term context
*/
updateMediumTermContext(message) {
// Add new message
this.mediumTermContext.push(message);
// Limit to max messages
if (this.mediumTermContext.length > this.maxMediumTermMessages) {
this.mediumTermContext = this.mediumTermContext.slice(-this.maxMediumTermMessages);
}
}
/**
* Sets the user's persona or core knowledge as long-term context
* @param personaContent The persona or core knowledge content
*/
setPersona(personaContent) {
// Remove any existing persona messages
this.longTermContext = this.longTermContext.filter((msg) => msg.role !== 'system' ||
!msg.content.includes('You are an assistant that can help answer questions using the contents of a vault'));
// Add the new persona
this.longTermContext.push({
role: 'system',
content: personaContent,
});
}
/**
* Gets the combined conversation context for the current turn
* @param userMessage The user's current message
* @returns Complete conversation context with all three layers
*/
getConversationContext(_userMessage) {
return {
shortTermContext: this.shortTermContext,
mediumTermContext: this.mediumTermContext,
longTermContext: this.longTermContext,
};
}
/**
* Gets the complete messages array for sending to the LLM
* @param userMessage The user's current message
* @returns Complete message array for the LLM
*/
getCompleteMessages(userMessage) {
const userMessageWithContext = {
role: 'user',
content: userMessage,
};
// Build messages in the proper order:
// 1. Long-term context (user persona, system instructions)
// 2. Medium-term context (session knowledge base query results)
// 3. Short-term context (last N turns)
// 4. Current user message
return [
...this.longTermContext,
...this.mediumTermContext,
...this.shortTermContext,
userMessageWithContext,
];
}
/**
* Clears all conversation context
*/
clear() {
this.shortTermContext = [];
this.mediumTermContext = [];
this.longTermContext = [
{
role: 'system',
content: `You are an assistant that can help answer questions using the contents of a vault.
The user can ask questions about their vault contents, and you should provide helpful responses based on the files.
When a user asks for information, try to find relevant files using the search_vault_files tool and read their contents with the read_vault_file tool.
Only use the tools if you need to access vault content that is not already in the context.`,
},
];
}
/**
* Sets the medium-term context from a knowledge base query result
* @param queryResult The result from a knowledge base query
*/
setMediumTermContextFromQuery(queryResult) {
// Clear previous medium-term context
this.mediumTermContext = [];
// Add the query result as context
if (queryResult.trim()) {
this.mediumTermContext.push({
role: 'system',
content: `Knowledge base results for current query:\n${queryResult}`,
});
}
}
/**
* Gets the current short-term context
*/
getShortTermContext() {
return [...this.shortTermContext];
}
/**
* Gets the current medium-term context
*/
getMediumTermContext() {
return [...this.mediumTermContext];
}
/**
* Gets the current long-term context
*/
getLongTermContext() {
return [...this.longTermContext];
}
}
exports.ConversationStateManager = ConversationStateManager;
-107
View File
@@ -1,107 +0,0 @@
"use strict";
// src/error-handler.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.ErrorHandler = void 0;
const obsidian_1 = require("obsidian");
const types_1 = require("./types");
class ErrorHandler {
static handleError(error, context) {
const message = this.getUserFriendlyMessage(error);
new obsidian_1.Notice(message);
if (error instanceof Error) {
const ctx = context ? ` [${context}]` : '';
// Use console.error instead of ErrorHandler.error for fatal errors
console.error(`Ollama Plugin Error${ctx}: ${error.message}`);
if (error.stack) {
console.error(error.stack);
}
}
else {
const ctx = context ? ` [${context}]` : '';
console.error(`Ollama Plugin Error${ctx}:`, error);
}
}
static getUserFriendlyMessage(error) {
if (error instanceof types_1.OllamaError) {
return this.getUserFriendlyMessageFromOllamaError(error);
}
if (error instanceof Error) {
return this.getUserFriendlyMessageFromError(error);
}
return 'An unexpected error occurred';
}
static getUserFriendlyMessageFromOllamaError(error) {
switch (error.type) {
case types_1.ErrorType.NETWORK_ERROR:
return 'Connection error. Please check if Ollama is running.';
case types_1.ErrorType.API_ERROR:
return `API error: ${error.message}`;
case types_1.ErrorType.VALIDATION_ERROR:
return this.getUserFriendlyValidationMessage(error);
case types_1.ErrorType.STREAMING_ERROR:
return 'Response too long. Please try a shorter request.';
case types_1.ErrorType.TOOL_EXECUTION_ERROR:
return `Tool error for ${error.toolName}. ${error.message}`;
case types_1.ErrorType.PATH_VALIDATION_ERROR:
return `Invalid file path: ${error.path}`;
case types_1.ErrorType.UNKNOWN_ERROR:
return 'An unexpected error occurred';
default:
return 'An unexpected error occurred';
}
}
static getUserFriendlyValidationMessage(error) {
if (error instanceof types_1.ValidationError && error.details?.field) {
const fieldMsg = error.details.field.charAt(0).toUpperCase() + error.details.field.slice(1);
return `Invalid ${fieldMsg.toLowerCase()}. ${error.details.message ?? error.message}`;
}
return 'Input validation error. Please correct your input.';
}
static getUserFriendlyMessageFromError(error) {
const msg = error.message.toLowerCase();
// Check timeout BEFORE network (more specific matches first)
if (msg.includes('timeout') || msg.includes('timed out') || msg.includes('time out')) {
return 'Request timed out. Please check your Ollama connection.';
}
if (msg.includes('network') || msg.includes('connection') || msg.includes('fetch')) {
return 'Connection error. Please check if Ollama is running.';
}
if (msg.includes('validation') || msg.includes('invalid')) {
return 'Invalid input. Please correct your input.';
}
if (msg.includes('stream') || msg.includes('chunk')) {
return 'Response too long. Please try a shorter request.';
}
if (msg.includes('tool') || msg.includes('function')) {
return 'Tool error. Please try again.';
}
if (msg.includes('path') || msg.includes('file')) {
return 'Invalid file path. Please check the path and try again.';
}
return 'An unexpected error occurred';
}
// -- Factory methods --
static createNetworkError(message, statusCode) {
return new types_1.NetworkError(message, statusCode);
}
static createApiError(message, statusCode) {
return new types_1.ApiError(message, statusCode ?? 500);
}
static createValidationError(message, field) {
const details = field ? { field, message } : undefined;
return new types_1.ValidationError(message, details);
}
static createStreamingError(message) {
return new types_1.StreamingError(message);
}
static createToolExecutionError(message, toolName) {
return new types_1.ToolExecutionError(message, toolName ?? 'unknown');
}
static createPathValidationError(message, path) {
return new types_1.PathValidationError(message, path ?? '');
}
static createUnknownError(message) {
return new types_1.OllamaError(message, types_1.ErrorType.UNKNOWN_ERROR);
}
}
exports.ErrorHandler = ErrorHandler;
-90
View File
@@ -1,90 +0,0 @@
"use strict";
// src/indexing-pipeline/extraction.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContentExtractor = void 0;
/**
* Extracts raw content from a vault file including:
* - Markdown content
* - YAML frontmatter
* - Headings
* - Embedded code blocks
* - First paragraph
*/
class ContentExtractor {
extractFromFile(file, content) {
const frontmatter = {};
const headings = [];
const embeddedCodeBlocks = [];
let firstParagraph;
// Extract frontmatter
const frontmatterMatch = content.match(/^---(.*?)---/s);
if (frontmatterMatch) {
try {
const frontmatterContent = frontmatterMatch[1];
const lines = frontmatterContent.trim().split('\n');
for (const line of lines) {
const [key, ...valueParts] = line.split(':');
if (!key)
continue;
const value = valueParts.join(':').trim();
if (key.trim() === 'title') {
if (value) {
frontmatter.title = value;
}
}
else if (key.trim() === 'tags') {
if (value) {
frontmatter.tags = value;
}
}
else {
// Store other frontmatter fields as-is
frontmatter[key.trim()] = value;
}
}
}
catch {
// If frontmatter parsing fails, continue with empty frontmatter
}
}
// Extract headings
const headingMatches = content.match(/^#{1,6} (.*?)$/gm);
if (headingMatches) {
headings.push(...headingMatches.map((h) => h.replace(/^#{1,6} /, '')));
}
// Extract embedded code blocks
const codeBlockMatches = content.match(/```([\s\S]*?)```/g);
if (codeBlockMatches) {
embeddedCodeBlocks.push(...codeBlockMatches);
}
// Extract first paragraph
const paragraphMatch = content.match(/^([^#]*?)(?=#|\n\n|$)/s);
if (paragraphMatch) {
firstParagraph = paragraphMatch[1].trim();
}
return {
basename: file.basename,
path: file.path,
content,
frontmatter,
headings,
embeddedCodeBlocks,
firstParagraph,
};
}
/**
* Extracts just the raw text content without headers, frontmatter, etc.
*/
extractRawText(content) {
return content
.replace(/^---.*?---/s, '')
.replace(/^#.*?$/gm, '')
.replace(/```.*?```/gs, '')
.replace(/`.*?`/g, '')
.replace(/\[(.*?)\]\(.*?\)/g, '$1')
.replace(/\*\*(.*?)\*\*/g, '$1')
.replace(/\*(.*?)\*/g, '$1')
.trim();
}
}
exports.ContentExtractor = ContentExtractor;
-12
View File
@@ -1,12 +0,0 @@
"use strict";
// src/indexing-pipeline/index.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.IndexingPipeline = exports.ContentVectorizer = exports.ContentNormalizer = exports.ContentExtractor = void 0;
var extraction_1 = require("./extraction");
Object.defineProperty(exports, "ContentExtractor", { enumerable: true, get: function () { return extraction_1.ContentExtractor; } });
var normalization_1 = require("./normalization");
Object.defineProperty(exports, "ContentNormalizer", { enumerable: true, get: function () { return normalization_1.ContentNormalizer; } });
var vectorization_1 = require("./vectorization");
Object.defineProperty(exports, "ContentVectorizer", { enumerable: true, get: function () { return vectorization_1.ContentVectorizer; } });
var pipeline_1 = require("./pipeline");
Object.defineProperty(exports, "IndexingPipeline", { enumerable: true, get: function () { return pipeline_1.IndexingPipeline; } });
-159
View File
@@ -1,159 +0,0 @@
"use strict";
// src/indexing-pipeline/normalization.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContentNormalizer = void 0;
/**
* Normalizes and enriches extracted content
*/
class ContentNormalizer {
/**
* Normalizes content by:
* - Standardizing dates to ISO 8601
* - Converting to lowercase for tokenization
* - Extracting tokens
* - Adding metadata
*/
normalize(extractedContent) {
const { basename, path, content, frontmatter, headings, firstParagraph } = extractedContent;
// Standardize title (remove .md extension)
const title = basename.replace(/\.md$/, '');
// Extract tokens (lowercase, remove stop words, etc.)
const tokens = this.tokenize(content);
// Normalize dates (if present in frontmatter)
const normalizedFrontmatter = this.normalizeFrontmatter(frontmatter);
// Calculate word count
const wordCount = content.split(/\s+/).filter(Boolean).length;
return {
path,
title,
content,
tokens,
headings,
frontmatter: normalizedFrontmatter,
firstParagraph,
wordCount,
// Add timestamps if available in frontmatter
createdAt: this.extractDate(frontmatter, 'created') || this.extractDate(frontmatter, 'date'),
updatedAt: this.extractDate(frontmatter, 'updated'),
};
}
/**
* Tokenizes text content by splitting on whitespace and removing stop words
*/
tokenize(text) {
const stopWords = new Set([
'the',
'a',
'an',
'and',
'or',
'but',
'is',
'are',
'was',
'were',
'in',
'on',
'at',
'to',
'of',
'for',
'with',
'as',
'by',
'it',
'its',
'that',
'this',
'these',
'those',
'from',
'up',
'out',
'off',
'over',
'under',
'again',
'further',
'then',
'once',
'here',
'there',
'when',
'where',
'why',
'how',
'all',
'any',
'both',
'each',
'few',
'more',
'most',
'other',
'some',
'such',
'no',
'nor',
'not',
'only',
'own',
'same',
'so',
'than',
'too',
'very',
'just',
'now',
]);
return text
.toLowerCase()
.split(/\W+/)
.filter((token) => token.length > 1 && !stopWords.has(token));
}
/**
* Normalizes frontmatter by standardizing data types and formats
*/
normalizeFrontmatter(frontmatter) {
const normalized = {};
for (const [key, value] of Object.entries(frontmatter)) {
if (key === 'tags' && typeof value === 'string') {
// Convert tag string to array if needed
normalized.tags = value.split(',').map((tag) => tag.trim());
}
else if (key === 'date' || key === 'created' || key === 'updated') {
// Try to parse and standardize date formats
if (typeof value === 'string') {
const date = new Date(value);
if (!isNaN(date.getTime())) {
normalized[key] = date.toISOString();
}
else {
normalized[key] = value; // Keep original if invalid date
}
}
else {
normalized[key] = value;
}
}
else {
normalized[key] = value;
}
}
return normalized;
}
/**
* Extracts a date from frontmatter
*/
extractDate(frontmatter, key) {
const value = frontmatter[key];
if (typeof value === 'string') {
const date = new Date(value);
if (!isNaN(date.getTime())) {
return date.toISOString();
}
}
return undefined;
}
}
exports.ContentNormalizer = ContentNormalizer;
-67
View File
@@ -1,67 +0,0 @@
"use strict";
// src/indexing-pipeline/pipeline.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.IndexingPipeline = void 0;
const extraction_1 = require("./extraction");
const normalization_1 = require("./normalization");
const vectorization_1 = require("./vectorization");
class IndexingPipeline {
constructor(config) {
this.extractor = new extraction_1.ContentExtractor();
this.normalizer = new normalization_1.ContentNormalizer();
this.vectorizer = new vectorization_1.ContentVectorizer({
model: config.embeddingModel,
ollamaUrl: config.ollamaUrl,
});
}
/**
* Processes a vault file through the entire pipeline
*/
processFile(file, content) {
try {
if (!content.trim()) {
return null;
}
// Extraction step
const extracted = this.extractor.extractFromFile(file, content);
// Normalization/Enrichment step
const normalized = this.normalizer.normalize(extracted);
// Return the normalized content as an index entry
return {
path: normalized.path,
title: normalized.title,
content: this.extractor.extractRawText(content).substring(0, 500),
score: 0, // Score will be calculated during search
};
}
catch {
return null;
}
}
/**
* Processes multiple files in batches
*/
processFilesInBatches(files, fileContents, batchSize = 10) {
const results = [];
const seenPaths = new Set();
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
const batchResults = batch.map((file) => {
const content = fileContents[file.path];
if (!content) {
return null;
}
const entry = this.processFile(file, content);
if (entry && !seenPaths.has(entry.path)) {
seenPaths.add(entry.path);
return entry;
}
return null;
});
const validResults = batchResults.filter((result) => result !== null);
results.push(...validResults);
}
return results;
}
}
exports.IndexingPipeline = IndexingPipeline;
-65
View File
@@ -1,65 +0,0 @@
"use strict";
// src/indexing-pipeline/vectorization.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContentVectorizer = void 0;
const utils_1 = require("../utils");
/**
* Vectorizes content chunks using Ollama embeddings
*/
class ContentVectorizer {
constructor(config, fetchFn) {
this.model = config.model;
this.ollamaUrl = config.ollamaUrl;
this.fetchFn = fetchFn ?? fetch;
}
/**
* Generates embeddings for a content chunk
*/
async vectorize(chunk) {
try {
const prompt = this.createPrompt(chunk);
const response = await this.fetchFn(`${this.ollamaUrl}/api/embeddings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: this.model,
prompt: prompt,
}),
});
if (!response.ok) {
throw new Error(`Embedding failed with status ${response.status}`);
}
const data = await response.json();
if (!this.isEmbeddingResponse(data)) {
throw new Error('Invalid embedding response');
}
return data.embedding;
}
catch (error) {
// Return empty array on failure to maintain compatibility
utils_1.Logger.warn(`Failed to generate embedding: ${String(error)}`, 'indexing-pipeline');
return [];
}
}
isEmbeddingResponse(data) {
return (typeof data === 'object' &&
data !== null &&
Array.isArray(data.embedding) &&
data.embedding.every((value) => typeof value === 'number'));
}
/**
* Creates a prompt from content chunk for embedding
*/
createPrompt(chunk) {
// Combine important elements for embedding
const parts = [
chunk.title,
chunk.firstParagraph,
chunk.content.substring(0, 1000), // Limit content to avoid long prompts
chunk.headings.join(' '),
JSON.stringify(chunk.frontmatter),
].filter(Boolean);
return parts.join('\n\n');
}
}
exports.ContentVectorizer = ContentVectorizer;
-198
View File
@@ -1,198 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const obsidian_1 = require("obsidian");
const chat_view_1 = require("./chat-view");
const constants_1 = require("./constants");
const semantic_cache_1 = require("./semantic-cache");
class OllamaPlugin extends obsidian_1.Plugin {
constructor() {
super(...arguments);
this.settings = constants_1.DEFAULT_SETTINGS;
}
async onload() {
await this.loadSettings();
// Register the chat view
this.registerView('ollama-chat-view', (leaf) => new chat_view_1.ChatView(leaf, this.settings));
// Add a command to open the chat view
this.addCommand({
id: 'open-ollama-chat',
name: 'Open Ollama Chat',
callback: async () => {
await this.activateChatView();
},
});
// Add a command to clear the semantic cache
this.addCommand({
id: 'clear-semantic-cache',
name: 'Clear Semantic Cache',
callback: async () => {
await this.clearSemanticCache();
new obsidian_1.Notice('Semantic cache cleared.');
},
});
// Add a settings tab
this.addSettingTab(new OllamaSettingTab(this.app, this));
// Initialize the semantic cache
if (this.settings.cacheConfig) {
this.semanticCache = new semantic_cache_1.SemanticCacheService(this.settings.ollamaUrl, this.settings.cacheConfig);
try {
await this.semanticCache.initialize();
}
catch {
new obsidian_1.Notice('Semantic cache initialization failed. Check console for details.');
}
}
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onunload() {
// Clean up any active semantic cache resources on plugin unload
// Using fire-and-forget pattern since onunload cannot be async per Obsidian API
if (this.semanticCache) {
void this.semanticCache.clearCache();
}
// No explicit unregisterView needed; relying on Obsidian lifecycle management.
}
async loadSettings() {
const loadedSettings = ((await this.loadData()) ?? {});
this.settings = Object.assign({}, constants_1.DEFAULT_SETTINGS, loadedSettings);
}
async saveSettings() {
await this.saveData(this.settings);
}
async activateChatView() {
const existing = this.app.workspace.getLeavesOfType('ollama-chat-view');
if (existing.length > 0) {
await this.app.workspace.revealLeaf(existing[0]);
}
else {
const leaf = this.app.workspace.getRightLeaf(false);
if (leaf) {
await leaf.setViewState({
type: 'ollama-chat-view',
active: true,
});
}
}
}
async clearSemanticCache() {
if (this.semanticCache) {
await this.semanticCache.clearCache();
}
}
notifyChatViews() {
const leaves = this.app.workspace.getLeavesOfType('ollama-chat-view');
leaves.forEach((leaf) => {
if (leaf.view instanceof chat_view_1.ChatView) {
leaf.view.updateSettings(this.settings);
}
});
}
}
exports.default = OllamaPlugin;
class OllamaSettingTab extends obsidian_1.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Ollama Settings' });
new obsidian_1.Setting(containerEl)
.setName('Ollama URL')
.setDesc('URL for your Ollama instance (default: http://localhost:11434)')
.addText((text) => text.setValue(this.plugin.settings.ollamaUrl).onChange(async (value) => {
this.plugin.settings.ollamaUrl = value;
await this.plugin.saveSettings();
}));
new obsidian_1.Setting(containerEl)
.setName('Model')
.setDesc('Ollama model to use (default: llama3)')
.addText((text) => text.setValue(this.plugin.settings.model).onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
}));
new obsidian_1.Setting(containerEl)
.setName('Vault Search Limit')
.setDesc('Maximum number of vault entries to include in context (default: 3)')
.addText((text) => text.setValue(String(this.plugin.settings.vaultSearchLimit)).onChange(async (value) => {
const parsed = parseInt(value);
if (!isNaN(parsed) && parsed > 0) {
this.plugin.settings.vaultSearchLimit = parsed;
await this.plugin.saveSettings();
}
else {
new obsidian_1.Notice('Vault search limit must be a positive integer.');
}
}));
new obsidian_1.Setting(containerEl)
.setName('Max Message History')
.setDesc('Maximum number of messages to keep in conversation history (default: 50)')
.addText((text) => text.setValue(String(this.plugin.settings.maxMessageHistory)).onChange(async (value) => {
const parsed = parseInt(value);
if (!isNaN(parsed) && parsed > 0) {
this.plugin.settings.maxMessageHistory = parsed;
await this.plugin.saveSettings();
}
else {
new obsidian_1.Notice('Max message history must be a positive integer.');
}
}));
new obsidian_1.Setting(containerEl)
.setName('Enable Semantic Cache')
.setDesc('Use semantic cache to store and retrieve previous responses')
.addToggle((toggle) => toggle.setValue(this.plugin.settings.cacheConfig.enabled).onChange(async (value) => {
this.plugin.settings.cacheConfig.enabled = value;
await this.plugin.saveSettings();
this.plugin.notifyChatViews();
}));
new obsidian_1.Setting(containerEl)
.setName('ChromaDB URL')
.setDesc('URL for your ChromaDB instance (default: http://localhost:8000)')
.addText((text) => text
.setValue(this.plugin.settings.cacheConfig.chromaURL || 'http://localhost:8000')
.onChange(async (value) => {
this.plugin.settings.cacheConfig.chromaURL = value;
await this.plugin.saveSettings();
}));
new obsidian_1.Setting(containerEl)
.setName('Cache Embedding Model')
.setDesc('Ollama model used to generate embeddings for the semantic cache')
.addText((text) => text.setValue(this.plugin.settings.cacheConfig.embeddingModel).onChange(async (value) => {
this.plugin.settings.cacheConfig.embeddingModel = value;
await this.plugin.saveSettings();
this.plugin.notifyChatViews();
}));
new obsidian_1.Setting(containerEl)
.setName('Cache Similarity Threshold')
.setDesc('Minimum cosine similarity (01) for a cache hit. Higher values require closer matches.')
.addText((text) => text
.setValue(String(this.plugin.settings.cacheConfig.similarityThreshold))
.onChange(async (value) => {
const parsed = parseFloat(value);
if (!isNaN(parsed) && parsed >= 0 && parsed <= 1) {
this.plugin.settings.cacheConfig.similarityThreshold = parsed;
await this.plugin.saveSettings();
}
else {
new obsidian_1.Notice('Similarity threshold must be a number between 0 and 1.');
}
}));
new obsidian_1.Setting(containerEl)
.setName('Clear Semantic Cache')
.setDesc('Delete all cached responses from ChromaDB')
.addButton((button) => button.setButtonText('Clear Cache').onClick(async () => {
try {
await this.plugin.clearSemanticCache();
new obsidian_1.Notice('Semantic cache cleared.');
}
catch {
new obsidian_1.Notice('Failed to clear semantic cache. Is ChromaDB running?');
}
}));
}
hide() {
// Clear the container to prevent duplicate elements
this.containerEl.empty();
}
}
-294
View File
@@ -1,294 +0,0 @@
"use strict";
// src/ollama-client.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.OllamaClient = void 0;
const types_1 = require("./types");
const utils_1 = require("./utils");
const semantic_cache_1 = require("./semantic-cache");
class OllamaClient {
constructor(baseURL, model, fetchFn, cacheConfig) {
this.maxRetries = 3;
this.maxMalformedChunks = 50;
this.currentStreamController = null;
this.baseURL = baseURL;
this.model = model;
this.fetchFn = fetchFn ?? fetch;
if (cacheConfig?.enabled) {
this.cacheService = new semantic_cache_1.SemanticCacheService(baseURL, cacheConfig);
void this.cacheService.initialize();
}
}
async initializeCache() {
if (this.cacheService) {
await this.cacheService.initialize();
}
}
async clearCache() {
if (this.cacheService) {
await this.cacheService.clearCache();
}
}
cancelStream() {
if (this.currentStreamController) {
this.currentStreamController.abort();
this.currentStreamController = null;
}
}
async *streamChat(messages, tools = []) {
// Bypass cache if tools are involved to prevent state corruption
if (tools.length > 0) {
yield* this.streamChatWithRetry(messages, tools, 0);
return;
}
// Find the last user message
const lastUserMsg = [...messages].reverse().find((m) => m.role === 'user');
if (lastUserMsg && this.cacheService) {
const cached = await this.cacheService.getCache(lastUserMsg.content);
if (cached) {
yield { role: 'assistant', content: cached, tool_calls: [] };
return;
}
}
const chunks = [];
for await (const chunk of this.streamChatWithRetry(messages, tools, 0)) {
chunks.push(chunk);
yield chunk;
}
const fullContent = chunks.map((c) => c.content).join('');
if (this.cacheService && lastUserMsg) {
void this.cacheService.setCache(lastUserMsg.content, fullContent);
}
}
async chat(messages, tools = []) {
// Bypass cache if tools are involved to prevent state corruption
if (tools.length > 0) {
return this.chatWithRetry(messages, tools, 0);
}
// Find the last user message
const lastUserMsg = [...messages].reverse().find((m) => m.role === 'user');
if (lastUserMsg && this.cacheService) {
const cached = await this.cacheService.getCache(lastUserMsg.content);
if (cached) {
return { role: 'assistant', content: cached };
}
}
const response = await this.chatWithRetry(messages, tools, 0);
if (this.cacheService && lastUserMsg) {
void this.cacheService.setCache(lastUserMsg.content, response.content);
}
return response;
}
async streamChatAsPromise(messages, tools = []) {
let content = '';
let role = 'assistant';
let toolCalls;
for await (const chunk of this.streamChat(messages, tools)) {
role = chunk.role ?? role;
content += chunk.content ?? '';
if (chunk.tool_calls) {
toolCalls = [...(toolCalls ?? []), ...chunk.tool_calls];
}
}
return { role, content, tool_calls: toolCalls };
}
async *streamChatWithRetry(messages, tools = [], retryCount) {
const controller = new AbortController();
this.currentStreamController = controller;
let reader = null;
try {
const response = await this.fetchFn(`${this.baseURL}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: this.model,
messages: messages,
tools: tools,
stream: true,
}),
signal: controller.signal,
});
if (!response.ok) {
throw new types_1.ApiError(`Ollama API error: ${response.status}`, response.status);
}
if (!response.body) {
throw new Error('No response body');
}
const contentType = response.headers?.get?.('content-type');
if (contentType && !contentType.includes('application/x-ndjson')) {
throw new Error('Invalid response format');
}
reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let malformedChunks = 0;
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value);
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') {
continue;
}
let parsed;
try {
parsed = this.parseChatResponse(line);
}
catch (error) {
malformedChunks++;
const errorMessage = error instanceof Error ? error.message : String(error);
utils_1.Logger.warn(`Skipped malformed chunk: ${line.slice(0, 50)}... - ${errorMessage}`, 'ollama-client');
if (malformedChunks > this.maxMalformedChunks) {
throw new Error('Too many malformed chunks in Ollama response');
}
continue;
}
if (parsed.error) {
throw new Error(`Ollama error: ${parsed.error}`);
}
yield this.normalizeMessage(parsed.message);
}
}
if (buffer.trim() !== '') {
let parsed = null;
try {
parsed = this.parseChatResponse(buffer);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
utils_1.Logger.warn(`Skipped malformed chunk: ${buffer.slice(0, 50)}... - ${errorMessage}`, 'ollama-client');
}
if (parsed?.error) {
throw new Error(`Ollama error: ${parsed.error}`);
}
if (parsed?.message) {
yield this.normalizeMessage(parsed.message);
}
}
}
catch (error) {
if (retryCount < this.maxRetries && this.isRetryableError(error, controller)) {
const errorMessage = error instanceof Error ? error.message : String(error);
utils_1.Logger.warn(`Retrying after error (attempt ${retryCount + 1}): ${errorMessage}`, 'ollama-client');
await new Promise((resolve) => setTimeout(resolve, 10 * Math.pow(2, retryCount)));
yield* this.streamChatWithRetry(messages, tools, retryCount + 1);
}
else {
throw error;
}
}
finally {
reader?.releaseLock();
if (this.currentStreamController === controller) {
this.currentStreamController = null;
}
}
}
async chatWithRetry(messages, tools = [], retryCount) {
const controller = new AbortController();
this.currentStreamController = controller;
try {
const response = await this.fetchFn(`${this.baseURL}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: this.model,
messages: messages,
tools: tools,
stream: false,
}),
signal: controller.signal,
});
if (!response.ok) {
throw new types_1.ApiError(`Ollama API error: ${response.status}`, response.status);
}
const data = await response.json();
if (!this.isChatResponse(data)) {
return this.normalizeMessage();
}
return this.normalizeMessage(data.message);
}
catch (error) {
if (retryCount < this.maxRetries && this.isRetryableError(error, controller)) {
const errorMessage = error instanceof Error ? error.message : String(error);
utils_1.Logger.warn(`Retrying after error (attempt ${retryCount + 1}): ${errorMessage}`, 'ollama-client');
await new Promise((resolve) => setTimeout(resolve, 10 * Math.pow(2, retryCount)));
return this.chatWithRetry(messages, tools, retryCount + 1);
}
else {
throw error;
}
}
finally {
if (this.currentStreamController === controller) {
this.currentStreamController = null;
}
}
}
normalizeMessage(message) {
return {
role: message?.role ?? 'assistant',
content: message?.content ?? '',
tool_calls: message?.tool_calls ?? [],
tool_call_id: message?.tool_call_id,
};
}
parseChatResponse(raw) {
const parsed = JSON.parse(raw);
if (!this.isChatResponse(parsed)) {
throw new Error('Invalid chat response');
}
return parsed;
}
isChatResponse(data) {
if (typeof data !== 'object' || data === null) {
return false;
}
const response = data;
return ((response.error === undefined || typeof response.error === 'string') &&
(response.message === undefined || this.isPartialMessage(response.message)));
}
isPartialMessage(data) {
if (typeof data !== 'object' || data === null) {
return false;
}
const message = data;
const validRole = message.role === undefined ||
message.role === 'system' ||
message.role === 'user' ||
message.role === 'assistant' ||
message.role === 'tool';
return (validRole &&
(message.content === undefined || typeof message.content === 'string') &&
(message.tool_calls === undefined || Array.isArray(message.tool_calls)) &&
(message.tool_call_id === undefined || typeof message.tool_call_id === 'string'));
}
isRetryableError(error, controller) {
if (controller.signal.aborted) {
return false;
}
if (error instanceof types_1.ApiError && error.statusCode >= 400 && error.statusCode < 500) {
return false;
}
if (error instanceof Error) {
if (error.name === 'AbortError') {
return false;
}
if (error.message.startsWith('Ollama error:') ||
error.message.includes('Too many malformed chunks') ||
error.message === 'No response body' ||
error.message === 'Invalid response format') {
return false;
}
}
return true;
}
}
exports.OllamaClient = OllamaClient;
-105
View File
@@ -1,105 +0,0 @@
"use strict";
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return
/* eslint-disable */
// src/semantic-cache.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.SemanticCacheService = void 0;
const chromadb_1 = require("chromadb");
const utils_1 = require("./utils");
class SemanticCacheService {
constructor(ollamaURL, config) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.collection = null;
this.ollamaURL = ollamaURL.replace(/\/+$/, '');
this.config = config;
// Use configurable ChromaDB URL or default to localhost
const chromaURL = config.chromaURL || 'http://localhost:8000';
this.client = new chromadb_1.ChromaClient({ path: chromaURL });
}
async initialize() {
if (!this.config.enabled)
return;
try {
this.collection = await this.client.getOrCreateCollection({
name: this.config.collectionName,
metadata: { 'hnsw:space': 'cosine' },
});
utils_1.Logger.info(`Semantic cache initialized: ${this.config.collectionName}`, 'semantic-cache');
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
utils_1.Logger.error(`Failed to initialize semantic cache: ${errorMessage}`, 'semantic-cache');
throw error;
}
}
async getCache(query) {
if (!this.config.enabled || !this.collection)
return null;
try {
const results = await this.collection.query({
query_embeddings: await this.generateEmbedding(query),
n_results: 1,
where: { source: 'ollama' },
});
if (results.ids[0] && results.ids[0].length > 0) {
const [id] = results.ids[0];
const [content] = results.documents[0];
if (results.distances[0] && results.distances[0][0] > this.config.similarityThreshold) {
return content;
}
}
return null;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
utils_1.Logger.warn(`Cache lookup failed: ${errorMessage}`, 'semantic-cache');
return null;
}
}
async setCache(query, response) {
if (!this.config.enabled || !this.collection)
return;
try {
await this.collection.add({
ids: [crypto.randomUUID()],
documents: [response],
embeddings: await this.generateEmbedding(query),
metadatas: [{ source: 'ollama' }],
});
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
utils_1.Logger.warn(`Cache set failed: ${errorMessage}`, 'semantic-cache');
}
}
async clearCache() {
if (!this.config.enabled || !this.collection)
return;
try {
await this.collection.reset();
utils_1.Logger.info('Semantic cache cleared', 'semantic-cache');
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
utils_1.Logger.error(`Failed to clear semantic cache: ${errorMessage}`, 'semantic-cache');
}
}
async generateEmbedding(text) {
const response = await fetch(`${this.ollamaURL}/api/embeddings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: this.config.embeddingModel,
prompt: text,
}),
});
if (!response.ok) {
throw new Error(`Failed to generate embedding: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data.embedding;
}
}
exports.SemanticCacheService = SemanticCacheService;
-167
View File
@@ -1,167 +0,0 @@
"use strict";
// src/tool-executor.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.ToolExecutor = void 0;
const obsidian_1 = require("obsidian");
const utils_1 = require("./utils");
// Disallow characters that are invalid in file paths
const INVALID_PATH_CHARS = /[<>:"|?*~]/;
const MAX_PATH_LENGTH = 200;
const FORBIDDEN_DIRS = ['.obsidian', '.git'];
class ToolExecutor {
constructor(vault, app) {
this.vault = vault;
this.app = app;
}
isSafePath(path) {
// Reject empty paths
if (!path || path.trim().length === 0) {
return false;
}
// Reject paths that are too long
if (path.length > MAX_PATH_LENGTH) {
return false;
}
// Reject paths with invalid characters
if (INVALID_PATH_CHARS.test(path)) {
return false;
}
// Reject absolute paths
if (path.startsWith('/') || path.startsWith('\\')) {
return false;
}
// Reject Windows drive letters (e.g., C:)
if (/^[a-zA-Z]:/.test(path)) {
return false;
}
// Reject paths containing backslashes (Windows-style path separators)
if (path.includes('\\')) {
return false;
}
// Reject paths that traverse to parent directories
const normalized = path.replace(/^(\.\/)+/, '');
if (normalized.split('/').includes('..')) {
return false;
}
// Reject forbidden directories
for (const dir of FORBIDDEN_DIRS) {
if (normalized.startsWith(`${dir}/`) || normalized.startsWith(`${dir}\\`)) {
return false;
}
if (normalized.includes(`/${dir}/`) || normalized.includes(`\\${dir}\\`)) {
return false;
}
}
return true;
}
async handleToolCall(toolCall) {
try {
const toolName = toolCall.function?.name;
const rawArgs = toolCall.function?.arguments;
if (!toolName) {
throw new Error('Tool name is required');
}
// Parse arguments whether they're a string or object
let parsedArgs;
if (typeof rawArgs === 'string') {
try {
parsedArgs = (0, utils_1.safeParseJson)(rawArgs);
}
catch {
throw new Error('Invalid JSON arguments');
}
}
else if (rawArgs && typeof rawArgs === 'object') {
parsedArgs = rawArgs;
}
else {
throw new Error('Arguments must be an object or JSON string');
}
// Process the tool call based on its type
switch (toolName) {
case 'create_file':
return await this.handleCreateFile(parsedArgs);
case 'read_vault_file':
return await this.handleReadVaultFile(parsedArgs);
case 'search_vault_files':
return this.handleSearchVaultFiles(parsedArgs);
default:
return { success: false, message: `Unknown tool: ${toolName}` };
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(errorMessage);
}
}
async handleCreateFile(args) {
const path = args.path;
const content = args.content;
if (typeof path !== 'string') {
throw new Error('Path must be a string');
}
if (typeof content !== 'string') {
throw new Error('Content must be a string');
}
if (!this.isSafePath(path)) {
throw new Error('Invalid file path detected');
}
try {
await this.vault.create(path, content);
return { success: true, message: 'File created successfully' };
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(errorMessage);
}
}
async executeTool(name, args) {
return this.handleToolCall({
id: crypto.randomUUID(),
type: 'function',
function: {
name,
arguments: args,
},
});
}
async handleReadVaultFile(args) {
const path = args.path;
if (typeof path !== 'string') {
throw new Error('Path must be a string');
}
if (!this.isSafePath(path)) {
throw new Error('Invalid file path detected');
}
const file = this.vault.getAbstractFileByPath(path);
if (!(file instanceof obsidian_1.TFile)) {
throw new Error(`File not found: ${path}`);
}
const content = await this.vault.cachedRead(file);
return {
success: true,
message: 'File read successfully',
data: { path, content },
};
}
handleSearchVaultFiles(args) {
const query = args.query;
const limitArg = args.limit;
if (typeof query !== 'string') {
throw new Error('Query must be a string');
}
const limit = typeof limitArg === 'number' && Number.isFinite(limitArg) ? limitArg : 10;
const normalizedQuery = query.toLowerCase();
const files = this.vault
.getMarkdownFiles()
.filter((file) => file.path.toLowerCase().includes(normalizedQuery))
.slice(0, limit)
.map((file) => ({ path: file.path, basename: file.basename }));
return {
success: true,
message: `Found ${files.length} matching files`,
data: files,
};
}
}
exports.ToolExecutor = ToolExecutor;
-72
View File
@@ -1,72 +0,0 @@
"use strict";
// src/types.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.PathValidationError = exports.ToolExecutionError = exports.StreamingError = exports.ValidationError = exports.ApiError = exports.NetworkError = exports.OllamaError = exports.ErrorType = void 0;
// ============================================================
// Error Type Hierarchy
// ============================================================
var ErrorType;
(function (ErrorType) {
ErrorType["NETWORK_ERROR"] = "network_error";
ErrorType["API_ERROR"] = "api_error";
ErrorType["VALIDATION_ERROR"] = "validation_error";
ErrorType["STREAMING_ERROR"] = "streaming_error";
ErrorType["TOOL_EXECUTION_ERROR"] = "tool_execution_error";
ErrorType["PATH_VALIDATION_ERROR"] = "path_validation_error";
ErrorType["UNKNOWN_ERROR"] = "unknown_error";
})(ErrorType || (exports.ErrorType = ErrorType = {}));
class OllamaError extends Error {
constructor(message, type) {
super(message);
this.type = type;
Object.setPrototypeOf(this, OllamaError.prototype);
}
}
exports.OllamaError = OllamaError;
class NetworkError extends OllamaError {
constructor(message, statusCode) {
super(message, ErrorType.NETWORK_ERROR);
this.statusCode = statusCode;
Object.setPrototypeOf(this, NetworkError.prototype);
}
}
exports.NetworkError = NetworkError;
class ApiError extends OllamaError {
constructor(message, statusCode) {
super(message, ErrorType.API_ERROR);
this.statusCode = statusCode;
Object.setPrototypeOf(this, ApiError.prototype);
}
}
exports.ApiError = ApiError;
class ValidationError extends OllamaError {
constructor(message, details) {
super(message, ErrorType.VALIDATION_ERROR);
this.details = details;
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
exports.ValidationError = ValidationError;
class StreamingError extends OllamaError {
constructor(message) {
super(message, ErrorType.STREAMING_ERROR);
Object.setPrototypeOf(this, StreamingError.prototype);
}
}
exports.StreamingError = StreamingError;
class ToolExecutionError extends OllamaError {
constructor(message, toolName = 'unknown') {
super(message, ErrorType.TOOL_EXECUTION_ERROR);
this.toolName = toolName;
Object.setPrototypeOf(this, ToolExecutionError.prototype);
}
}
exports.ToolExecutionError = ToolExecutionError;
class PathValidationError extends OllamaError {
constructor(message, path = '') {
super(message, ErrorType.PATH_VALIDATION_ERROR);
this.path = path;
Object.setPrototypeOf(this, PathValidationError.prototype);
}
}
exports.PathValidationError = PathValidationError;
-168
View File
@@ -1,168 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Logger = exports.LogLevel = void 0;
exports.validateOllamaUrl = validateOllamaUrl;
exports.validateModelName = validateModelName;
exports.validatePluginSettings = validatePluginSettings;
exports.safeParseJson = safeParseJson;
var LogLevel;
(function (LogLevel) {
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
LogLevel[LogLevel["INFO"] = 1] = "INFO";
LogLevel[LogLevel["WARN"] = 2] = "WARN";
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
})(LogLevel || (exports.LogLevel = LogLevel = {}));
const SEVERITY_ORDER = {
debug: LogLevel.DEBUG,
info: LogLevel.INFO,
warn: LogLevel.WARN,
error: LogLevel.ERROR,
};
class Logger {
static setLevel(level) {
if (typeof level === 'string') {
const lowerLevel = level.toLowerCase();
Logger.minLevel = SEVERITY_ORDER[lowerLevel] ?? LogLevel.DEBUG;
}
else {
Logger.minLevel = level;
}
}
static debug(message, category = 'general') {
if (LogLevel.DEBUG >= Logger.minLevel) {
console.debug(`[${category}] DEBUG: ${message}`);
}
}
static info(message, category = 'general') {
if (LogLevel.INFO >= Logger.minLevel) {
console.info(`[${category}] INFO: ${message}`);
}
}
static warn(message, category = 'general') {
if (LogLevel.WARN >= Logger.minLevel) {
console.warn(`[${category}] WARN: ${message}`);
}
}
static error(message, category = 'general') {
if (LogLevel.ERROR >= Logger.minLevel) {
console.error(`[${category}] ERROR: ${message}`);
}
}
}
exports.Logger = Logger;
Logger.minLevel = LogLevel.DEBUG;
// ==================== URL & Model Validation ====================
function validateOllamaUrl(url) {
if (typeof url !== 'string' || !url.trim()) {
return { valid: false, error: 'URL cannot be empty' };
}
const trimmedUrl = url.trim();
if (trimmedUrl.endsWith('/')) {
return { valid: false, error: 'URL should not end with a slash' };
}
try {
const parsed = new URL(trimmedUrl);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' };
}
return { valid: true };
}
catch {
return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' };
}
}
function validateModelName(model) {
if (typeof model !== 'string') {
return { valid: false, error: 'Model name must be a string' };
}
const trimmedModel = model.trim();
// Explicit check for empty string after trimming
if (!trimmedModel || trimmedModel.length === 0) {
return { valid: false, error: 'Model name cannot be empty' };
}
if (trimmedModel.length < 2) {
return { valid: false, error: 'Model name must be at least 2 characters long' };
}
if (trimmedModel.length > 100) {
return { valid: false, error: 'Model name must be less than 100 characters long' };
}
if (!/^[a-zA-Z0-9._:-]+$/.test(trimmedModel)) {
return {
valid: false,
error: 'Model name can only contain letters, numbers, dots, dashes, underscores, and colons',
};
}
return { valid: true };
}
function validatePluginSettings(settings) {
const errors = [];
const urlValidation = validateOllamaUrl(settings.ollamaUrl);
if (!urlValidation.valid) {
errors.push(`Invalid Ollama URL: ${urlValidation.error}`);
}
const modelValidation = validateModelName(settings.model);
if (!modelValidation.valid) {
errors.push(`Invalid Model Name: ${modelValidation.error}`);
}
return errors;
}
// ==================== Safe JSON Parsing ====================
const MAX_JSON_SIZE = 1000000;
const MAX_JSON_NESTING = 24;
function countNestingDepth(value, depth = 0) {
if (depth > MAX_JSON_NESTING) {
return depth;
}
if (Array.isArray(value)) {
return Math.max(...value.map((item) => countNestingDepth(item, depth + 1)), depth);
}
if (value !== null && typeof value === 'object') {
const entries = Object.values(value);
if (entries.length === 0)
return depth;
return Math.max(...entries.map((item) => countNestingDepth(item, depth + 1)), depth);
}
return depth;
}
function safeParseJson(jsonString) {
if (typeof jsonString !== 'string') {
throw new Error('Input must be a string');
}
if (jsonString.length > MAX_JSON_SIZE) {
throw new Error('JSON input too large');
}
let parsed;
try {
parsed = JSON.parse(jsonString);
}
catch {
throw new Error('Invalid JSON');
}
// Check for dangerous prototype pollution patterns in object keys only
const checkDangerousPatterns = (obj) => {
if (typeof obj !== 'object' || obj === null) {
return false;
}
const dangerousKeys = ['constructor', 'prototype', '__proto__'];
if (dangerousKeys.some((key) => Object.keys(obj).includes(key))) {
return true;
}
// Recursively check nested objects (own properties only)
const record = obj;
for (const key of Object.keys(obj)) {
if (checkDangerousPatterns(record[key])) {
return true;
}
}
return false;
};
if (checkDangerousPatterns(parsed)) {
throw new Error('dangerous code pattern detected');
}
// Check nesting depth
if (countNestingDepth(parsed) > MAX_JSON_NESTING) {
throw new Error('JSON nesting too deep');
}
return parsed;
}
// ==================== Markdown Utilities ====================
-226
View File
@@ -1,226 +0,0 @@
"use strict";
// src/vault-indexer.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.VaultIndexer = exports.InMemoryCache = void 0;
const utils_1 = require("./utils");
class InMemoryCache {
constructor() {
this.store = new Map();
}
get(key) {
return Promise.resolve(this.store.get(key) ?? null);
}
put(key, value) {
this.store.set(key, value);
return Promise.resolve();
}
}
exports.InMemoryCache = InMemoryCache;
const STOP_WORDS = new Set([
'a', 'an', 'the', 'is', 'it', 'in', 'on', 'at', 'to', 'for', 'of',
'and', 'or', 'but', 'with', 'by', 'from', 'up', 'about', 'into',
'this', 'that', 'these', 'those', 'be', 'been', 'being', 'have',
'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should',
'may', 'might', 'can', 'are', 'was', 'were', 'as', 'so', 'if', 'not',
'no', 'my', 'your', 'our', 'its', 'we', 'you', 'he', 'she', 'they',
]);
const CONTENT_PREVIEW_LENGTH = 500;
class VaultIndexer {
constructor(vault, cache) {
this.SCORING_WEIGHTS = {
TITLE: 5,
FRONTMATTER_TITLE: 4,
FRONTMATTER_TAGS: 3,
HEADINGS: 2,
CONTENT: 1,
};
this.vault = vault;
this.cache = cache;
}
tokenize(text) {
return text
.toLowerCase()
.replace(/[^\w\s]/g, '')
.split(/\s+/)
.filter((token) => token.length > 1 && !STOP_WORDS.has(token));
}
tokenizeContent(content, file) {
const parsed = this.parseMarkdown(content);
const bodyWithoutFrontmatter = content.replace(/^---[\s\S]*?---\n/, '');
const paragraphs = bodyWithoutFrontmatter
.split(/\n\n+/)
.map((p) => p.trim())
.filter((p) => p && !p.startsWith('#'));
const firstParagraph = paragraphs[0] || '';
return {
title: parsed.title || file.basename,
headings: parsed.headings,
frontmatter: parsed.frontmatter,
firstParagraph,
content: parsed.content,
basename: file.basename,
};
}
calculateWeightedScore(tokenized, queryTokens) {
let score = 0;
for (const token of queryTokens) {
if (tokenized.frontmatter?.title && this.exactMatch(tokenized.frontmatter.title, token)) {
score += this.SCORING_WEIGHTS.FRONTMATTER_TITLE;
}
if (tokenized.basename && this.exactMatch(tokenized.basename, token)) {
score += this.SCORING_WEIGHTS.FRONTMATTER_TITLE;
}
if (tokenized.frontmatter?.tags && this.exactMatch(tokenized.frontmatter.tags, token)) {
score += this.SCORING_WEIGHTS.FRONTMATTER_TAGS;
}
if (tokenized.headings.some((h) => h.toLowerCase().includes(token.toLowerCase()))) {
score += this.SCORING_WEIGHTS.HEADINGS;
}
if (tokenized.content.toLowerCase().includes(token.toLowerCase())) {
score += this.SCORING_WEIGHTS.CONTENT;
}
if (tokenized.title && this.exactMatch(tokenized.title, token)) {
score += this.SCORING_WEIGHTS.TITLE;
}
}
return { score };
}
async getVaultEntries() {
const files = this.vault.getMarkdownFiles();
const entries = [];
for (const file of files) {
try {
const content = typeof this.vault.cachedRead === 'function'
? await this.vault.cachedRead(file)
: await this.vault.read(file);
const parsed = this.parseMarkdown(content);
entries.push({
file: file,
title: parsed.frontmatter.title || file.basename,
frontmatter: parsed.frontmatter,
headings: parsed.headings,
content: parsed.content.slice(0, CONTENT_PREVIEW_LENGTH),
basename: file.basename,
score: 0,
});
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
utils_1.Logger.warn(`Failed to read file ${file.path}: ${errorMessage}`, 'vault-indexer');
}
}
return entries;
}
async searchVault(query, limit = 3) {
if (!query || !query.trim()) {
return [];
}
const cacheKey = `query:${query.trim()}:limit:${limit}`;
if (this.cache) {
let cachedResults = null;
try {
cachedResults = await this.cache.get(cacheKey);
}
catch {
cachedResults = null;
}
if (cachedResults) {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const parsedResults = JSON.parse(cachedResults);
return parsedResults.slice(0, limit);
}
catch {
// ignore parse errors
}
}
}
const queryTokens = this.tokenize(query);
if (queryTokens.length === 0) {
return [];
}
const entries = await this.getVaultEntries();
const scored = entries
.map((entry) => {
const { score } = this.calculateWeightedScore({
title: entry.title,
headings: entry.headings,
frontmatter: entry.frontmatter,
firstParagraph: '',
content: entry.content,
basename: entry.basename,
}, queryTokens);
return { ...entry, score };
})
.filter((e) => e.score > 0);
scored.sort((a, b) => b.score - a.score);
const results = scored.slice(0, limit);
if (this.cache) {
try {
await this.cache.put(cacheKey, JSON.stringify(results));
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
utils_1.Logger.warn(`Failed to cache results for query "${query}": ${errorMessage}`, 'vault-indexer');
}
}
return results;
}
stemToken(token) {
if (token.endsWith('ing') && token.length > 4)
return token.slice(0, -3);
if (token.endsWith('ed') && token.length > 3)
return token.slice(0, -2);
if (token.endsWith('s') && token.length > 2)
return token.slice(0, -1);
return token;
}
exactMatch(text, queryToken) {
if (!text)
return false;
const textLower = text.toLowerCase();
const queryLower = queryToken.toLowerCase();
const queryStem = this.stemToken(queryLower);
return textLower.includes(queryLower) || textLower.includes(queryStem);
}
parseMarkdown(content) {
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n/;
const frontmatterMatch = content.match(frontmatterRegex);
const frontmatter = {};
if (frontmatterMatch) {
try {
const lines = frontmatterMatch[1].trim().split('\n');
for (const line of lines) {
const [key, ...valueParts] = line.split(':');
if (!key)
continue;
const value = valueParts.join(':').trim();
if (key.trim() === 'title' && value)
frontmatter.title = value;
else if (key.trim() === 'tags' && value)
frontmatter.tags = value;
}
}
catch {
utils_1.Logger.warn('Failed to parse frontmatter', 'vault-indexer');
}
}
const titleMatch = content.match(/^# (.+)$/m);
const title = titleMatch ? titleMatch[1] : '';
const headings = [];
const headingRegex = /^#{1,6} (.+)$/gm;
let headingMatch;
while ((headingMatch = headingRegex.exec(content)) !== null) {
headings.push(headingMatch[1]);
}
const bodyWithoutFrontmatter = frontmatterMatch
? content.substring(frontmatterMatch[0].length)
: content;
const bodyText = bodyWithoutFrontmatter
.replace(/#{1,6} .+/g, '')
.replace(/^\s*[\r\n]/gm, '')
.trim();
return { frontmatter, title, headings, content: bodyText };
}
}
exports.VaultIndexer = VaultIndexer;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -1,18 +1,20 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ES2020",
"lib": ["ESNext", "DOM"],
"module": "commonjs", "module": "commonjs",
"outDir": "./src", "outDir": "./dist",
"rootDir": "./src", "rootDir": "./src",
"strict": true, "strict": true,
"types": ["node", "jest"],
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"moduleResolution": "node", "moduleResolution": "node",
"lib": ["ESNext", "DOM"],
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"typeRoots": ["node_modules/@types", "./src"], "exclude": ["node_modules", "tests", "__mocks__", "dist"],
"exclude": ["node_modules"],
} }