From 140198177b0955e5d645b9f5ceaea31b25177d6a Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Thu, 7 May 2026 14:16:53 +0200 Subject: [PATCH] Guard cleanup of currentStreamController after abort --- .gitignore | 1 + CHANGES.md | 38 - coverage/lcov-report/base.css | 224 --- coverage/lcov-report/block-navigation.js | 87 -- coverage/lcov-report/chat-view.ts.html | 1516 -------------------- coverage/lcov-report/error-handler.ts.html | 490 ------- coverage/lcov-report/favicon.png | Bin 445 -> 0 bytes coverage/lcov-report/index.html | 206 --- coverage/lcov-report/ollama-client.ts.html | 946 ------------ coverage/lcov-report/prettify.css | 1 - coverage/lcov-report/prettify.js | 2 - coverage/lcov-report/sort-arrow-sprite.png | Bin 138 -> 0 bytes coverage/lcov-report/sorter.js | 210 --- coverage/lcov-report/tool-executor.ts.html | 478 ------ coverage/lcov-report/types.ts.html | 619 -------- coverage/lcov-report/utils.ts.html | 658 --------- coverage/lcov-report/vault-indexer.ts.html | 1198 ---------------- coverage/lcov.info | 1289 ----------------- src/ollama-client.ts | 6 +- tests/ollama-client.test.ts | 96 ++ 20 files changed, 101 insertions(+), 7964 deletions(-) delete mode 100755 CHANGES.md delete mode 100644 coverage/lcov-report/base.css delete mode 100644 coverage/lcov-report/block-navigation.js delete mode 100644 coverage/lcov-report/chat-view.ts.html delete mode 100644 coverage/lcov-report/error-handler.ts.html delete mode 100644 coverage/lcov-report/favicon.png delete mode 100644 coverage/lcov-report/index.html delete mode 100644 coverage/lcov-report/ollama-client.ts.html delete mode 100644 coverage/lcov-report/prettify.css delete mode 100644 coverage/lcov-report/prettify.js delete mode 100644 coverage/lcov-report/sort-arrow-sprite.png delete mode 100644 coverage/lcov-report/sorter.js delete mode 100644 coverage/lcov-report/tool-executor.ts.html delete mode 100644 coverage/lcov-report/types.ts.html delete mode 100644 coverage/lcov-report/utils.ts.html delete mode 100644 coverage/lcov-report/vault-indexer.ts.html delete mode 100644 coverage/lcov.info diff --git a/.gitignore b/.gitignore index eca2080..a549d13 100755 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ npm-debug.log* # review agents agent_loop_gemma4/ +rules diff --git a/CHANGES.md b/CHANGES.md deleted file mode 100755 index 84b6170..0000000 --- a/CHANGES.md +++ /dev/null @@ -1,38 +0,0 @@ -# Changes - -## [Unreleased] - -### Fixed - -- **CRITICAL**: Fixed `removeEventListeners` in `chat-view.ts` which was incorrectly adding listeners instead of removing them, causing memory leaks and duplicate event handlers -- **CRITICAL**: Added guards in `setupEventListeners` to prevent duplicate event listeners from accumulating on repeated open/close cycles -- **CRITICAL**: Added path sanitization in `tool-executor.ts` to prevent path traversal attacks by validating that paths are relative and don't contain `..` segments -- **MAJOR**: Added proper cleanup in `ollama-client.ts` async generator with try/finally block to ensure `reader.releaseLock()` and abort controller cleanup on parse errors -- **MAJOR**: Made `fetchFn` injectable in `OllamaClient` constructor to improve testability - -### Added - -- **MAJOR**: Added conversation history management with "New Chat" button to clear messages and start fresh conversations -- **MINOR**: Added `.gitignore` file to exclude backup files, build artifacts, and other unwanted files -- **MINOR**: Created `README.md` with setup instructions and usage documentation -- **MINOR**: Added sensible default values for Ollama URL (`http://localhost:11434`) and model (`llama3`) to prevent confusing failures -- **NIT**: Added `CHANGES.md` file to document changes - -### Changed - -- **MINOR**: Fixed `tsconfig.json` to include `main.ts` in the root directory instead of incorrectly scoping to `./src` -- **NIT**: Refactored code formatting and structure for better readability - -### Security - -- Added path validation to prevent directory traversal attacks in file creation tool -- Added content type validation to ensure proper argument parsing - -### Technical Debt - -- Removed committed backup file (`src/chat-view.ts.backup`) -- Removed references to hardcoded paths and improved modularity - -## [Previous Versions] - -No formal versioning was maintained prior to these changes. diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css deleted file mode 100644 index f418035..0000000 --- a/coverage/lcov-report/base.css +++ /dev/null @@ -1,224 +0,0 @@ -body, html { - margin:0; padding: 0; - height: 100%; -} -body { - font-family: Helvetica Neue, Helvetica, Arial; - font-size: 14px; - color:#333; -} -.small { font-size: 12px; } -*, *:after, *:before { - -webkit-box-sizing:border-box; - -moz-box-sizing:border-box; - box-sizing:border-box; - } -h1 { font-size: 20px; margin: 0;} -h2 { font-size: 14px; } -pre { - font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; - margin: 0; - padding: 0; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; -} -a { color:#0074D9; text-decoration:none; } -a:hover { text-decoration:underline; } -.strong { font-weight: bold; } -.space-top1 { padding: 10px 0 0 0; } -.pad2y { padding: 20px 0; } -.pad1y { padding: 10px 0; } -.pad2x { padding: 0 20px; } -.pad2 { padding: 20px; } -.pad1 { padding: 10px; } -.space-left2 { padding-left:55px; } -.space-right2 { padding-right:20px; } -.center { text-align:center; } -.clearfix { display:block; } -.clearfix:after { - content:''; - display:block; - height:0; - clear:both; - visibility:hidden; - } -.fl { float: left; } -@media only screen and (max-width:640px) { - .col3 { width:100%; max-width:100%; } - .hide-mobile { display:none!important; } -} - -.quiet { - color: #7f7f7f; - color: rgba(0,0,0,0.5); -} -.quiet a { opacity: 0.7; } - -.fraction { - font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; - font-size: 10px; - color: #555; - background: #E8E8E8; - padding: 4px 5px; - border-radius: 3px; - vertical-align: middle; -} - -div.path a:link, div.path a:visited { color: #333; } -table.coverage { - border-collapse: collapse; - margin: 10px 0 0 0; - padding: 0; -} - -table.coverage td { - margin: 0; - padding: 0; - vertical-align: top; -} -table.coverage td.line-count { - text-align: right; - padding: 0 5px 0 20px; -} -table.coverage td.line-coverage { - text-align: right; - padding-right: 10px; - min-width:20px; -} - -table.coverage td span.cline-any { - display: inline-block; - padding: 0 5px; - width: 100%; -} -.missing-if-branch { - display: inline-block; - margin-right: 5px; - border-radius: 3px; - position: relative; - padding: 0 4px; - background: #333; - color: yellow; -} - -.skip-if-branch { - display: none; - margin-right: 10px; - position: relative; - padding: 0 4px; - background: #ccc; - color: white; -} -.missing-if-branch .typ, .skip-if-branch .typ { - color: inherit !important; -} -.coverage-summary { - border-collapse: collapse; - width: 100%; -} -.coverage-summary tr { border-bottom: 1px solid #bbb; } -.keyline-all { border: 1px solid #ddd; } -.coverage-summary td, .coverage-summary th { padding: 10px; } -.coverage-summary tbody { border: 1px solid #bbb; } -.coverage-summary td { border-right: 1px solid #bbb; } -.coverage-summary td:last-child { border-right: none; } -.coverage-summary th { - text-align: left; - font-weight: normal; - white-space: nowrap; -} -.coverage-summary th.file { border-right: none !important; } -.coverage-summary th.pct { } -.coverage-summary th.pic, -.coverage-summary th.abs, -.coverage-summary td.pct, -.coverage-summary td.abs { text-align: right; } -.coverage-summary td.file { white-space: nowrap; } -.coverage-summary td.pic { min-width: 120px !important; } -.coverage-summary tfoot td { } - -.coverage-summary .sorter { - height: 10px; - width: 7px; - display: inline-block; - margin-left: 0.5em; - background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; -} -.coverage-summary .sorted .sorter { - background-position: 0 -20px; -} -.coverage-summary .sorted-desc .sorter { - background-position: 0 -10px; -} -.status-line { height: 10px; } -/* yellow */ -.cbranch-no { background: yellow !important; color: #111; } -/* dark red */ -.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } -.low .chart { border:1px solid #C21F39 } -.highlighted, -.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ - background: #C21F39 !important; -} -/* medium red */ -.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } -/* light red */ -.low, .cline-no { background:#FCE1E5 } -/* light green */ -.high, .cline-yes { background:rgb(230,245,208) } -/* medium green */ -.cstat-yes { background:rgb(161,215,106) } -/* dark green */ -.status-line.high, .high .cover-fill { background:rgb(77,146,33) } -.high .chart { border:1px solid rgb(77,146,33) } -/* dark yellow (gold) */ -.status-line.medium, .medium .cover-fill { background: #f9cd0b; } -.medium .chart { border:1px solid #f9cd0b; } -/* light yellow */ -.medium { background: #fff4c2; } - -.cstat-skip { background: #ddd; color: #111; } -.fstat-skip { background: #ddd; color: #111 !important; } -.cbranch-skip { background: #ddd !important; color: #111; } - -span.cline-neutral { background: #eaeaea; } - -.coverage-summary td.empty { - opacity: .5; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1; - color: #888; -} - -.cover-fill, .cover-empty { - display:inline-block; - height: 12px; -} -.chart { - line-height: 0; -} -.cover-empty { - background: white; -} -.cover-full { - border-right: none !important; -} -pre.prettyprint { - border: none !important; - padding: 0 !important; - margin: 0 !important; -} -.com { color: #999 !important; } -.ignore-none { color: #999; font-weight: normal; } - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -48px; -} -.footer, .push { - height: 48px; -} diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js deleted file mode 100644 index 530d1ed..0000000 --- a/coverage/lcov-report/block-navigation.js +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -var jumpToCode = (function init() { - // Classes of code we would like to highlight in the file view - var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; - - // Elements to highlight in the file listing view - var fileListingElements = ['td.pct.low']; - - // We don't want to select elements that are direct descendants of another match - var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` - - // Selector that finds elements on the page to which we can jump - var selector = - fileListingElements.join(', ') + - ', ' + - notSelector + - missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` - - // The NodeList of matching elements - var missingCoverageElements = document.querySelectorAll(selector); - - var currentIndex; - - function toggleClass(index) { - missingCoverageElements - .item(currentIndex) - .classList.remove('highlighted'); - missingCoverageElements.item(index).classList.add('highlighted'); - } - - function makeCurrent(index) { - toggleClass(index); - currentIndex = index; - missingCoverageElements.item(index).scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'center' - }); - } - - function goToPrevious() { - var nextIndex = 0; - if (typeof currentIndex !== 'number' || currentIndex === 0) { - nextIndex = missingCoverageElements.length - 1; - } else if (missingCoverageElements.length > 1) { - nextIndex = currentIndex - 1; - } - - makeCurrent(nextIndex); - } - - function goToNext() { - var nextIndex = 0; - - if ( - typeof currentIndex === 'number' && - currentIndex < missingCoverageElements.length - 1 - ) { - nextIndex = currentIndex + 1; - } - - makeCurrent(nextIndex); - } - - return function jump(event) { - if ( - document.getElementById('fileSearch') === document.activeElement && - document.activeElement != null - ) { - // if we're currently focused on the search input, we don't want to navigate - return; - } - - switch (event.which) { - case 78: // n - case 74: // j - goToNext(); - break; - case 66: // b - case 75: // k - case 80: // p - goToPrevious(); - break; - } - }; -})(); -window.addEventListener('keydown', jumpToCode); diff --git a/coverage/lcov-report/chat-view.ts.html b/coverage/lcov-report/chat-view.ts.html deleted file mode 100644 index af9ceaa..0000000 --- a/coverage/lcov-report/chat-view.ts.html +++ /dev/null @@ -1,1516 +0,0 @@ - - - - - - Code coverage report for chat-view.ts - - - - - - - - - -
-
-

All files chat-view.ts

-
- -
- 92.55% - Statements - 199/215 -
- - -
- 85.71% - Branches - 60/70 -
- - -
- 82.85% - Functions - 29/35 -
- - -
- 94.52% - Lines - 190/201 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 -449 -450 -451 -452 -453 -454 -455 -456 -457 -458 -459 -460 -461 -462 -463 -464 -465 -466 -467 -468 -469 -470 -471 -472 -473 -474 -475 -476 -477 -4781x -  -  -  -  -  -  -1x -1x -1x -  -  -  -  -  -  -  -  -1x -1x -1x -1x -  -1x -  -36x -  -  -  -36x -36x -36x -36x -36x -36x -36x -36x -36x -36x -36x -36x -  -  -  -2x -  -  -  -4x -  -  -  -2x -  -  -  -36x -36x -36x -36x -36x -  -  -  -  -  -  -  -  -1x -  -  -  -1x -  -  -  -5x -5x -5x -5x -  -  -  -  -  -  -  -3x -3x -3x -3x -3x -3x -3x -3x -  -  -  -  -146x -28x -  -  -  -  -  -  -  -43x -43x -  -43x -  -  -43x -4x -  -43x -4x -  -  -4x -  -  -43x -  -27x -  -27x -  -  -27x -27x -  -  -  -43x -  -  -282x -  -  -43x -  -43x -262x -262x -109x -  -153x -  -  -153x -153x -  -  -  -  -43x -133x -3661x -24x -  -  -  -  -282x -43x -1x -  -  -1x -1x -  -  -  -  -  -13x -  -  -13x -13x -2x -2x -2x -  -  -  -13x -13x -4x -2x -2x -2x -  -  -  -  -13x -  -  -13x -  -  -13x -  -  -  -  -13x -13x -13x -7x -7x -  -7x -  -13x -  -  -  -8x -2x -  -8x -2x -  -8x -2x -  -8x -8x -8x -8x -  -  -  -  -2x -2x -2x -2x -  -  -  -292x -26x -26x -  -  -  -  -26x -  -  -  -158x -19x -12x -  -  -12x -  -19x -19x -  -  -  -  -21x -21x -  -21x -  -21x -21x -  -  -19x -19x -  -  -19x -19x -  -  -  -19x -  -  -19x -  -  -  -19x -  -  -  -  -19x -  -  -  -120x -  -  -  -  -  -  -  -  -19x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -19x -  -19x -19x -  -  -19x -  -  -  -  -  -  -19x -  -  -  -  -  -  -  -  -19x -  -19x -19x -  -19x -19x -19x -19x -19x -17x -17x -  -  -  -17x -17x -  -  -17x -4x -  -  -17x -  -  -  -13x -  -  -  -  -  -13x -  -4x -4x -  -  -  -  -4x -6x -  -  -4x -4x -6x -2x -  -  -4x -  -  -  -  -4x -  -2x -  -  -2x -  -  -  -  -  -2x -2x -2x -  -  -2x -  -  -  -  -  -  -2x -  -  -  -  -  -  -  -13x -9x -  -  -  -  -  -13x -2x -  -13x -  -  -19x -  -  -  -6x -  -  -  -  -6x -12x -  -6x -  -21x -21x -  -  -  -  - 
import { ItemView, WorkspaceLeaf, Notice } from 'obsidian';
-/// <reference lib="dom" />
-// Use global types from JSDOM setup
-type KeyboardEvent = globalThis.KeyboardEvent;
-type HTMLTextAreaElement = globalThis.HTMLTextAreaElement;
-type HTMLButtonElement = globalThis.HTMLButtonElement;
- 
-const DEFAULT_VAULT_SEARCH_LIMIT = 3;
-const MAX_MESSAGE_HISTORY = 50;
-const MAX_STREAM_CHUNKS = 1000;
-import {
-  PluginSettings,
-  OllamaMessage,
-  ChatMessage,
-  OllamaTool,
-  ToolCall,
-  ToolResult,
-} from './types';
-import { OllamaClient } from './ollama-client';
-import { VaultIndexer } from './vault-indexer';
-import { ToolExecutor } from './tool-executor';
-import { ErrorHandler } from './error-handler';
- 
-export class ChatView extends ItemView {
-  private settings: PluginSettings;
-  private messages: ChatMessage[] = [];
-  private ollamaClient: OllamaClient;
-  private vaultIndexer: VaultIndexer;
-  private toolExecutor: ToolExecutor;
-  private lastMessageEl: HTMLElement | null = null;
-  private newChatButton: HTMLButtonElement | null = null;
-  private sendButton: HTMLButtonElement | null = null;
-  private inputEl: HTMLTextAreaElement | null = null;
-  private chatContainer: HTMLElement | null = null;
-  private sendButtonClickHandler: (() => Promise<void>) | null = null;
-  private inputKeyDownHandler: ((e: KeyboardEvent) => Promise<void>) | null = null;
-  private newChatButtonClickHandler: (() => void) | null = null;
-  private sendButtonClickWrapper: (() => void) | null = null;
-  private inputKeyDownWrapper: ((e: KeyboardEvent) => void) | null = null;
-  private newChatButtonClickWrapper: (() => void) | null = null;
-  private listenersAttached = false;
- 
-  // Getters for testing
-  public getSendButtonClickHandler(): (() => Promise<void>) | null {
-    return this.sendButtonClickHandler;
-  }
- 
-  public getInputKeyDownHandler(): ((e: KeyboardEvent) => Promise<void>) | null {
-    return this.inputKeyDownHandler;
-  }
- 
-  public getNewChatButtonClickHandler(): (() => void) | null {
-    return this.newChatButtonClickHandler;
-  }
- 
-  constructor(leaf: WorkspaceLeaf, settings: PluginSettings) {
-    super(leaf);
-    this.settings = settings;
-    this.ollamaClient = new OllamaClient(settings.ollamaUrl, settings.model);
-    this.vaultIndexer = new VaultIndexer(this.app.vault);
-    this.toolExecutor = new ToolExecutor(this.app.vault, this.app);
-  }
- 
-  public updateSettings(newSettings: PluginSettings): void {
-    this.settings = newSettings;
-    this.ollamaClient = new OllamaClient(newSettings.ollamaUrl, newSettings.model);
-  }
- 
-  getViewType(): string {
-    return 'ollama-chat-view';
-  }
- 
-  getDisplayText(): string {
-    return 'Ollama Chat';
-  }
- 
-  onOpen(): Promise<void> {
-    this.render();
-    this.removeEventListeners(); // Clean up any existing listeners before reattaching
-    this.setupEventListeners();
-    return Promise.resolve();
-  }
- 
-  public onSettingsChange(newSettings: PluginSettings): void {
-    this.updateSettings(newSettings);
-  }
- 
-  onClose(): Promise<void> {
-    this.ollamaClient.cancelStream();
-    this.removeEventListeners();
-    this.cleanupStreamingResources();
-    this.lastMessageEl = null;
-    this.sendButton = null;
-    this.inputEl = null;
-    this.chatContainer = null;
-    return Promise.resolve();
-  }
- 
-  private cleanupStreamingResources(): void {
-    // Only cleanup if there's still an active streaming message
-    const streamingMessage = this.messages.find((msg) => msg.isStreaming);
-    Iif (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' });
- 
-    if (!this.inputEl) {
-      this.inputEl = inputContainer.createEl('textarea', { cls: 'ollama-input' });
-    }
-    if (!this.sendButton) {
-      this.sendButton = inputContainer.createEl('button', {
-        cls: 'ollama-send-button',
-      });
-      this.sendButton.textContent = 'Send';
-    }
- 
-    if (!this.newChatButton) {
-      const newChatContainer =
-        this.contentEl.querySelector('.ollama-new-chat') ||
-        this.contentEl.createEl('div', { cls: 'ollama-new-chat' });
-      this.newChatButton = newChatContainer.createEl('button', {
-        cls: 'ollama-new-chat-button',
-      });
-      this.newChatButton.textContent = '🔄 New Chat';
-      this.newChatButton.title = 'Start a new conversation';
-    }
- 
-    // Create immutable snapshot for rendering
-    const messagesSnapshot = [...this.messages];
- 
-    // Only render messages that are not currently streaming
-    const nonStreamingMessages = messagesSnapshot.filter((msg) => !msg.isStreaming);
- 
-    // Differential update: only update messages that have changed
-    const existingMessages = container.querySelectorAll('.ollama-message');
- 
-    for (const msg of nonStreamingMessages) {
-      const existingEl = container.querySelector(`.ollama-message[data-msg-id="${msg.id}"]`);
-      if (existingEl) {
-        existingEl.textContent = msg.content;
-      } else {
-        const messageEl = container.createEl('div', {
-          cls: `ollama-message ${msg.role}`,
-        }) as HTMLElement;
-        messageEl.setAttribute('data-msg-id', msg.id);
-        messageEl.textContent = msg.content;
-      }
-    }
- 
-    // 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();
-      }
-    }
- 
-    // 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);
-      }
-    }
-  }
- 
-  private setupEventListeners(): void {
-    Iif (!this.sendButton || !this.inputEl || this.listenersAttached) return;
- 
-    // Create handlers if they don't exist
-    if (!this.sendButtonClickHandler) {
-      this.sendButtonClickHandler = async () => {
-        Iif (!this.inputEl) return;
-        await this.handleUserInput(this.inputEl.value);
-        this.inputEl.value = '';
-      };
-    }
- 
-    if (!this.inputKeyDownHandler) {
-      this.inputKeyDownHandler = async (e: KeyboardEvent) => {
-        if (!this.inputEl || e.key !== 'Enter' || e.shiftKey) return;
-        e.preventDefault();
-        await this.handleUserInput(this.inputEl.value);
-        this.inputEl.value = '';
-      };
-    }
- 
-    // Create wrapper functions for event listeners
-    this.sendButtonClickWrapper = () => {
-      void this.sendButtonClickHandler?.();
-    };
-    this.inputKeyDownWrapper = (e: KeyboardEvent) => {
-      void this.inputKeyDownHandler?.(e);
-    };
-    this.newChatButtonClickWrapper = () => {
-      void this.newChatButtonClickHandler?.();
-    };
- 
-    // Add event listeners using wrappers
-    this.sendButton.addEventListener('click', this.sendButtonClickWrapper);
-    this.inputEl.addEventListener('keydown', this.inputKeyDownWrapper);
-    if (this.newChatButton) {
-      if (!this.newChatButtonClickHandler) {
-        this.newChatButtonClickHandler = () => this.clearConversation();
-      }
-      this.newChatButton.addEventListener('click', this.newChatButtonClickWrapper);
-    }
-    this.listenersAttached = true;
-  }
- 
-  private removeEventListeners(): void {
-    if (this.sendButton && this.sendButtonClickWrapper) {
-      this.sendButton.removeEventListener('click', this.sendButtonClickWrapper);
-    }
-    if (this.inputEl && this.inputKeyDownWrapper) {
-      this.inputEl.removeEventListener('keydown', this.inputKeyDownWrapper);
-    }
-    if (this.newChatButton && this.newChatButtonClickWrapper) {
-      this.newChatButton.removeEventListener('click', this.newChatButtonClickWrapper);
-    }
-    this.sendButtonClickWrapper = null;
-    this.inputKeyDownWrapper = null;
-    this.newChatButtonClickWrapper = null;
-    this.listenersAttached = false;
-  }
- 
-  private clearConversation(): void {
-    // Create new array to ensure immutability
-    this.messages = [];
-    this.lastMessageEl = null;
-    this.render();
-    new Notice('Conversation cleared');
-  }
- 
-  private updateMessageById(id: string, partial: Partial<ChatMessage>): boolean {
-    const index = this.messages.findIndex((m) => m.id === id);
-    Iif (index < 0) return false;
-    this.messages = [
-      ...this.messages.slice(0, index),
-      { ...this.messages[index], ...partial },
-      ...this.messages.slice(index + 1),
-    ];
-    return true;
-  }
- 
-  private updateLastMessage(content: string) {
-    const streamingMessage = this.messages.find((msg) => msg.isStreaming);
-    if (streamingMessage && !this.lastMessageEl) {
-      this.lastMessageEl = this.contentEl.createEl('div', {
-        cls: `ollama-message assistant`,
-      }) as HTMLElement;
-      this.lastMessageEl.setAttribute('data-msg-id', streamingMessage.id);
-    }
-    if (this.lastMessageEl) {
-      this.lastMessageEl.textContent = content;
-    }
-  }
- 
-  private async handleUserInput(content: string) {
-    Iif (!this.sendButton || !this.inputEl) return;
-    this.sendButton.disabled = true;
- 
-    try {
-      // Guard against empty messages
-      const userMessage = content.trim();
-      if (!userMessage) return;
- 
-      // Search vault using user message as query
-      const entries = await this.vaultIndexer.searchVault(userMessage, DEFAULT_VAULT_SEARCH_LIMIT);
-      let context = entries.map((entry) => `### ${entry.title}\n${entry.content}`).join('\n\n');
- 
-      // Cap context size to prevent prompt bloat with large vaults
-      const MAX_CONTEXT_LENGTH = 4000;
-      Iif (context.length > MAX_CONTEXT_LENGTH) {
-        context = context.substring(0, MAX_CONTEXT_LENGTH) + '\n\n... (truncated)';
-      }
- 
-      const systemContent = context
-        ? `You are a helpful assistant.\n\nRelevant vault context:\n${context}`
-        : 'You are a helpful assistant.';
-      const systemMessage: OllamaMessage = {
-        role: 'system',
-        content: systemContent,
-      };
-      const userMessageWithContext: OllamaMessage = {
-        role: 'user',
-        content: userMessage,
-      };
- 
-      const messages: OllamaMessage[] = [
-        systemMessage,
-        ...this.messages.map(
-          (m) =>
-            ({
-              role: m.role,
-              content: m.content,
-              tool_calls: m.tool_calls,
-            }) as OllamaMessage
-        ),
-        userMessageWithContext,
-      ];
- 
-      const tools: OllamaTool[] = [
-        {
-          type: 'function',
-          function: {
-            name: 'create_file',
-            description: 'Create a new file in the vault',
-            parameters: {
-              type: 'object' as const,
-              properties: {
-                path: {
-                  type: 'string' as const,
-                  description: "Relative path within the vault, e.g. 'Notes/todo.md'",
-                },
-                content: { type: 'string' as const, description: 'Content of the file to create' },
-              },
-              required: ['path', 'content'],
-            },
-          },
-        },
-      ];
- 
-      const messageId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
- 
-      const userMessageId = messageId;
-      const assistantMessageId = `${messageId}-assistant`;
- 
-      // Store user message in conversation history
-      const userChatMessage: ChatMessage = {
-        id: userMessageId,
-        role: 'user' as const,
-        content: userMessage,
-        timestamp: Date.now(),
-      };
- 
-      const assistantMessage: ChatMessage = {
-        id: assistantMessageId,
-        role: 'assistant' as const,
-        content: '',
-        timestamp: Date.now(),
-        isStreaming: true,
-      };
- 
-      // Update messages immutably
-      this.messages = [...this.messages, userChatMessage, assistantMessage];
- 
-      try {
-        this.render();
- 
-        const stream = this.ollamaClient.streamChat(messages, tools);
-        let fullResponse = '';
-        let toolCalls: ToolCall[] = [];
-        let chunkCount = 0;
-        for await (const chunk of stream) {
-          chunkCount++;
-          Iif (chunkCount > MAX_STREAM_CHUNKS) {
-            throw new Error('Response too long, stopped streaming');
-          }
- 
-          if (chunk.content) {
-            fullResponse += chunk.content;
-          }
- 
-          if (chunk.tool_calls) {
-            toolCalls = toolCalls.concat(chunk.tool_calls);
-          }
- 
-          this.updateLastMessage(fullResponse);
-        }
- 
-        // Update the assistant message with the full response immutably
-        this.updateMessageById(assistantMessageId, {
-          content: fullResponse,
-          tool_calls: toolCalls,
-        });
- 
-        // Process tool calls with proper follow-up context
-        if (toolCalls.length > 0) {
-          // Validate tool calls before processing
-          const MAX_TOOL_CALLS = 10;
-          Iif (toolCalls.length > MAX_TOOL_CALLS) {
-            throw new Error(`Too many tool calls (max ${MAX_TOOL_CALLS})`);
-          }
- 
-          // Collect all tool results using allSettled to support partial results
-          const settledResults = await Promise.allSettled(
-            toolCalls.map((call) => this.toolExecutor.handleToolCall(call))
-          );
- 
-          const toolResults: ToolResult[] = [];
-          for (const result of settledResults) {
-            if (result.status === 'fulfilled') {
-              toolResults.push(result.value);
-            } else {
-              // Use centralized error handler for tool errors
-              ErrorHandler.handleError(result.reason, 'ChatView.handleUserInput');
-            }
-          }
- 
-          // Only create follow-up when we have tool results
-          if (toolResults.length > 0) {
-            // Create follow-up messages including the assistant's tool calls and results
-            const followUpMessages: OllamaMessage[] = [
-              ...messages,
-              { role: 'assistant' as const, content: fullResponse, tool_calls: toolCalls },
-              ...toolResults.map((result) => ({
-                role: 'tool' as const,
-                content: JSON.stringify(result),
-              })),
-            ];
- 
-            const followUp = await this.ollamaClient.chat(followUpMessages, tools);
-            fullResponse += followUp.content;
-            this.updateLastMessage(fullResponse);
- 
-            // Update the assistant message with the final response immutably
-            this.updateMessageById(assistantMessageId, {
-              content: fullResponse,
-              isStreaming: false,
-            });
-          } else {
-            // Even if no tool results were successful, mark streaming as complete
-            // to prevent the assistant message from disappearing
-            this.updateMessageById(assistantMessageId, {
-              content: fullResponse,
-              isStreaming: false,
-            });
-          }
-        }
- 
-        // Update assistant message immutably — only if no tool calls were processed
-        if (toolCalls.length === 0) {
-          this.updateMessageById(assistantMessageId, {
-            isStreaming: false,
-          });
-        }
- 
-        // Limit conversation history to prevent memory issues
-        if (this.messages.length > MAX_MESSAGE_HISTORY) {
-          this.messages = this.messages.slice(-MAX_MESSAGE_HISTORY);
-        }
-        this.render();
-      } finally {
-        // Clean up streaming resources regardless of outcome
-        this.cleanupStreamingResources();
-      }
-    } catch (error) {
-      // Use centralized error handler
-      ErrorHandler.handleError(error, 'ChatView.handleUserInput');
-      // Update any streaming messages to non-streaming state to prevent stale messages
-      // This ensures that if an error occurs during streaming, the assistant message
-      // is still visible (with any partial content received) but won't cause issues
-      // in subsequent requests due to stale isStreaming: true flag
-      this.messages = this.messages.map((msg) =>
-        msg.isStreaming ? { ...msg, isStreaming: false } : msg
-      );
-      this.cleanupStreamingResources();
-    } finally {
-      if (this.sendButton) {
-        this.sendButton.disabled = false;
-      }
-    }
-  }
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/error-handler.ts.html b/coverage/lcov-report/error-handler.ts.html deleted file mode 100644 index 748efcd..0000000 --- a/coverage/lcov-report/error-handler.ts.html +++ /dev/null @@ -1,490 +0,0 @@ - - - - - - Code coverage report for error-handler.ts - - - - - - - - - -
-
-

All files error-handler.ts

-
- -
- 94.23% - Statements - 49/52 -
- - -
- 87.5% - Branches - 42/48 -
- - -
- 100% - Functions - 12/12 -
- - -
- 94.23% - Lines - 49/52 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136  -  -2x -2x -  -  -  -  -  -  -  -  -  -  -2x -  -9x -9x -  -9x -8x -  -8x -8x -8x -  -  -1x -1x -  -  -  -  -15x -7x -  -  -8x -7x -  -  -1x -  -  -  -7x -  -2x -  -  -  -2x -  -1x -  -1x -  -1x -  -  -  -  -  -  -  -  -2x -1x -1x -  -1x -  -  -  -13x -  -  -13x -2x -  -  -11x -2x -  -  -9x -1x -  -  -8x -1x -  -  -7x -1x -  -  -6x -1x -  -  -5x -  -  -  -  -  -1x -  -  -  -1x -  -  -  -1x -1x -  -  -  -1x -  -  -  -1x -  -  -  -1x -  -  -  -1x -  -  - 
// src/error-handler.ts
- 
-import { Notice } from 'obsidian';
-import {
-  OllamaError,
-  ErrorType,
-  NetworkError,
-  ApiError,
-  ValidationError,
-  StreamingError,
-  ToolExecutionError,
-  PathValidationError,
-} from './types';
- 
-export class ErrorHandler {
-  static handleError(error: unknown, context?: string): void {
-    const message = this.getUserFriendlyMessage(error);
-    new 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);
-    }
-  }
- 
-  private static getUserFriendlyMessage(error: unknown): string {
-    if (error instanceof OllamaError) {
-      return this.getUserFriendlyMessageFromOllamaError(error);
-    }
- 
-    if (error instanceof Error) {
-      return this.getUserFriendlyMessageFromError(error);
-    }
- 
-    return 'An unexpected error occurred';
-  }
- 
-  private static getUserFriendlyMessageFromOllamaError(error: OllamaError): string {
-    switch (error.type) {
-      case ErrorType.NETWORK_ERROR:
-        return 'Connection error. Please check if Ollama is running.';
-      case ErrorType.API_ERROR:
-        return `API error: ${error.message}`;
-      case ErrorType.VALIDATION_ERROR:
-        return this.getUserFriendlyValidationMessage(error);
-      case ErrorType.STREAMING_ERROR:
-        return 'Response too long. Please try a shorter request.';
-      case ErrorType.TOOL_EXECUTION_ERROR:
-        return `Tool error for ${(error as ToolExecutionError).toolName}. ${error.message}`;
-      case ErrorType.PATH_VALIDATION_ERROR:
-        return `Invalid file path: ${(error as PathValidationError).path}`;
-      case ErrorType.UNKNOWN_ERROR:
-        return 'An unexpected error occurred';
-      default:
-        return 'An unexpected error occurred';
-    }
-  }
- 
-  private static getUserFriendlyValidationMessage(error: OllamaError): string {
-    if (error instanceof 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.';
-  }
- 
-  private static getUserFriendlyMessageFromError(error: Error): string {
-    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: string, statusCode?: number): NetworkError {
-    return new NetworkError(message, statusCode);
-  }
- 
-  static createApiError(message: string, statusCode?: number): ApiError {
-    return new ApiError(message, statusCode);
-  }
- 
-  static createValidationError(message: string, field?: string): ValidationError {
-    const details = field ? { field, message } : undefined;
-    return new ValidationError(message, details);
-  }
- 
-  static createStreamingError(message: string): StreamingError {
-    return new StreamingError(message);
-  }
- 
-  static createToolExecutionError(message: string, toolName?: string): ToolExecutionError {
-    return new ToolExecutionError(message, toolName ?? 'unknown');
-  }
- 
-  static createPathValidationError(message: string, path?: string): PathValidationError {
-    return new PathValidationError(message, path ?? '');
-  }
- 
-  static createUnknownError(message: string): OllamaError {
-    return new OllamaError(message, ErrorType.UNKNOWN_ERROR);
-  }
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png deleted file mode 100644 index c1525b811a167671e9de1fa78aab9f5c0b61cef7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> - - - - Code coverage report for All files - - - - - - - - - -
-
-

All files

-
- -
- 92.38% - Statements - 667/722 -
- - -
- 81.49% - Branches - 251/308 -
- - -
- 88.69% - Functions - 102/115 -
- - -
- 93.31% - Lines - 642/688 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
chat-view.ts -
-
92.55%199/21585.71%60/7082.85%29/3594.52%190/201
error-handler.ts -
-
94.23%49/5287.5%42/48100%12/1294.23%49/52
ollama-client.ts -
-
91.8%112/12272.72%40/5588.23%15/1791.37%106/116
tool-executor.ts -
-
91.37%53/5878.78%26/33100%4/491.37%53/58
types.ts -
-
100%35/35100%2/2100%8/8100%35/35
utils.ts -
-
95.65%88/9284.78%39/4693.33%14/1596.59%85/88
vault-indexer.ts -
-
88.51%131/14877.77%42/5483.33%20/2489.85%124/138
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/ollama-client.ts.html b/coverage/lcov-report/ollama-client.ts.html deleted file mode 100644 index f9abee0..0000000 --- a/coverage/lcov-report/ollama-client.ts.html +++ /dev/null @@ -1,946 +0,0 @@ - - - - - - Code coverage report for ollama-client.ts - - - - - - - - - -
-
-

All files ollama-client.ts

-
- -
- 91.8% - Statements - 112/122 -
- - -
- 72.72% - Branches - 40/55 -
- - -
- 88.23% - Functions - 15/17 -
- - -
- 91.37% - Lines - 106/116 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288  -  -  -2x -2x -  -  -  -  -  -2x -  -  -  -61x -61x -  -  -61x -61x -61x -  -  -  -6x -2x -2x -  -  -  -  -  -  -  -24x -  -  -  -  -  -  -3x -3x -  -  -  -  -  -  -  -  -  -  -  -29x -29x -  -29x -29x -  -  -  -  -  -  -  -  -  -  -  -  -  -27x -8x -5x -5x -  -  -  -5x -4x -4x -  -  -4x -4x -4x -4x -4x -  -  -4x -  -  -  -  -  -4x -  -  -  -  -  -5x -2x -  -3x -  -  -15x -1x -  -  -14x -14x -1x -  -  -13x -13x -13x -13x -13x -  -13x -13x -22x -22x -  -11x -  -11x -11x -  -11x -69x -  -68x -68x -15x -  -14x -14x -  -  -  -14x -14x -  -54x -1x -  -  -53x -53x -1x -  -  -52x -  -  -  -  -  -  -  -11x -1x -1x -1x -  -1x -1x -1x -  -  -  -  -  -  -  -  -  -  -  -  -13x -  -  -  -29x -  -  -11x -  -  -  -5x -  -  -  -  -  -  -  -  -8x -8x -8x -  -  -  -  -  -  -  -  -  -  -  -  -  -8x -4x -3x -3x -  -  -  -3x -2x -2x -  -  -2x -2x -2x -2x -2x -  -  -2x -  -  -  -  -  -2x -  -  -  -  -  -3x -  -1x -  -  -4x -4x -  -  -  -  -8x -  -  -  -  -16x -1x -  -  -  -  -19x -1x -  -  -18x -18x -  -  -  -  -  -  - 
// src/ollama-client.ts
- 
-import type { OllamaMessage, OllamaTool } from './types';
-import { ApiError } from './types';
-import { Logger } from './utils';
- 
-interface OllamaChatResponse {
-  message?: Partial<OllamaMessage>;
-}
- 
-export class OllamaClient {
-  private baseURL: string;
-  private model: string;
-  private fetchFn: typeof fetch;
-  private readonly maxRetries: number = 3;
-  private currentStreamController: AbortController | null = null;
- 
-  constructor(baseURL: string, model: string, fetchFn?: typeof fetch) {
-    this.baseURL = baseURL;
-    this.model = model;
-    this.fetchFn = fetchFn ?? fetch;
-  }
- 
-  cancelStream(): void {
-    if (this.currentStreamController) {
-      this.currentStreamController.abort();
-      this.currentStreamController = null;
-    }
-  }
- 
-  async *streamChat(
-    messages: OllamaMessage[],
-    tools: OllamaTool[] = []
-  ): AsyncGenerator<OllamaMessage, void, unknown> {
-    yield* this.streamChatWithRetry(messages, tools, 0);
-  }
- 
-  async streamChatAsPromise(
-    messages: OllamaMessage[],
-    tools: OllamaTool[] = []
-  ): Promise<OllamaMessage[]> {
-    const chunks: OllamaMessage[] = [];
-    for await (const chunk of this.streamChat(messages, tools)) {
-      chunks.push(chunk);
-    }
-    return chunks;
-  }
- 
-  private async *streamChatWithRetry(
-    messages: OllamaMessage[],
-    tools: OllamaTool[] = [],
-    attempt: number = 0
-  ): AsyncGenerator<OllamaMessage, void, unknown> {
-    // Create a local controller for this request instead of using the instance variable
-    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,
-          tools,
-          stream: true,
-        }),
-        signal: controller.signal,
-      });
- 
-      if (!response.ok) {
-        if (response.status >= 500 && attempt < this.maxRetries) {
-          const retryDelay = Math.pow(2, attempt) * 100;
-          Logger.warn(
-            `Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
-            'ollama-client'
-          );
-          if (attempt < this.maxRetries - 1) {
-            const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
-            const abortListener = () => {
-              Logger.info('Retry aborted by user', 'ollama-client');
-            };
-            const signal = controller.signal;
-            if (signal) {
-              signal.addEventListener('abort', abortListener);
-              try {
-                await Promise.race([
-                  retryTimeout,
-                  new Promise<void>((resolve) => {
-                    signal.addEventListener('abort', () => resolve(), {
-                      once: true,
-                    });
-                  }),
-                ]);
-              } finally {
-                signal.removeEventListener('abort', abortListener);
-              }
-            } else E{
-              await retryTimeout;
-            }
-          }
-          yield* this.streamChatWithRetry(messages, tools, attempt + 1);
-          return;
-        }
-        throw new 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('ndjson') && !contentType.includes('json'))) {
-        throw new Error('Invalid response format');
-      }
- 
-      const reader = response.body.getReader();
-      const decoder = new TextDecoder();
-      let buffer = '';
-      let malformedCount = 0;
-      const maxMalformed = 50;
- 
-      try {
-        while (true) {
-          const { done, value } = await reader.read();
-          if (done) break;
- 
-          buffer += decoder.decode(value, { stream: true });
- 
-          const lines = buffer.split('\n');
-          buffer = lines.pop() ?? '';
- 
-          for (const line of lines) {
-            if (!line.trim()) continue;
- 
-            try {
-              const parsed = JSON.parse(line) as Record<string, unknown>;
-              this.throwIfOllamaError(parsed);
- 
-              const message = this.toOllamaMessage(parsed.message);
-              Iif (!message) {
-                continue;
-              }
- 
-              malformedCount = 0;
-              yield message;
-            } catch (error) {
-              if (error instanceof Error && error.message.startsWith('Ollama error:')) {
-                throw error;
-              }
- 
-              malformedCount++;
-              if (malformedCount > maxMalformed) {
-                throw new Error('Too many malformed chunks in stream');
-              }
- 
-              Logger.warn(
-                `Skipped malformed chunk: ${line.substring(0, 80)}... - ${(error as Error).message}`,
-                'ollama-client'
-              );
-            }
-          }
-        }
- 
-        if (buffer.trim()) {
-          try {
-            const parsed = JSON.parse(buffer) as Record<string, unknown>;
-            this.throwIfOllamaError(parsed);
- 
-            const message = this.toOllamaMessage(parsed.message);
-            if (message) {
-              yield message;
-            }
-          } catch (error) {
-            Iif (error instanceof Error && error.message.startsWith('Ollama error:')) {
-              throw error;
-            }
-            Logger.warn(
-              `Failed to parse final chunk: ${buffer.substring(0, 80)}...`,
-              'ollama-client'
-            );
-          }
-        }
-      } finally {
-        reader.releaseLock();
-      }
-    } finally {
-      // Abort the local controller
-      controller.abort();
-    }
-    // Clean up the reference
-    this.currentStreamController = null;
-  }
- 
-  async chat(messages: OllamaMessage[], tools: OllamaTool[] = []): Promise<OllamaMessage> {
-    return this.chatWithRetry(messages, tools, 0);
-  }
- 
-  private async chatWithRetry(
-    messages: OllamaMessage[],
-    tools: OllamaTool[] = [],
-    attempt: number = 0
-  ): Promise<OllamaMessage> {
-    // Create a local controller for this request instead of using the instance variable
-    const controller = new AbortController();
-    try {
-      const response = await this.fetchFn(`${this.baseURL}/api/chat`, {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-        },
-        body: JSON.stringify({
-          model: this.model,
-          messages,
-          tools,
-          stream: false,
-        }),
-        signal: controller.signal,
-      });
- 
-      if (!response.ok) {
-        if (response.status >= 500 && attempt < this.maxRetries) {
-          const retryDelay = Math.pow(2, attempt) * 100;
-          Logger.warn(
-            `Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
-            'ollama-client'
-          );
-          if (attempt < this.maxRetries - 1) {
-            const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
-            const abortListener = () => {
-              Logger.info('Retry aborted by user', 'ollama-client');
-            };
-            const signal = controller.signal;
-            if (signal) {
-              signal.addEventListener('abort', abortListener);
-              try {
-                await Promise.race([
-                  retryTimeout,
-                  new Promise<void>((resolve) => {
-                    signal.addEventListener('abort', () => resolve(), {
-                      once: true,
-                    });
-                  }),
-                ]);
-              } finally {
-                signal.removeEventListener('abort', abortListener);
-              }
-            } else E{
-              await retryTimeout;
-            }
-          }
-          return this.chatWithRetry(messages, tools, attempt + 1);
-        }
-        throw new ApiError(`Ollama API error: ${response.status}`, response.status);
-      }
- 
-      const data = (await response.json()) as OllamaChatResponse;
-      return (
-        this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] }
-      );
-    } finally {
-      // Abort the local controller
-      controller.abort();
-    }
-  }
- 
-  private throwIfOllamaError(parsed: Record<string, unknown>): void {
-    if (parsed.error) {
-      throw new Error(`Ollama error: ${String(parsed.error)}`);
-    }
-  }
- 
-  private toOllamaMessage(value: unknown): OllamaMessage | null {
-    if (!value || typeof value !== 'object') {
-      return null;
-    }
- 
-    const record = value as Partial<OllamaMessage>;
-    return {
-      role: record.role ?? 'assistant',
-      content: typeof record.content === 'string' ? record.content : '',
-      tool_calls: record.tool_calls ?? [],
-    };
-  }
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/prettify.css b/coverage/lcov-report/prettify.css deleted file mode 100644 index b317a7c..0000000 --- a/coverage/lcov-report/prettify.css +++ /dev/null @@ -1 +0,0 @@ -.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/lcov-report/prettify.js b/coverage/lcov-report/prettify.js deleted file mode 100644 index b322523..0000000 --- a/coverage/lcov-report/prettify.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable */ -window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/lcov-report/sort-arrow-sprite.png b/coverage/lcov-report/sort-arrow-sprite.png deleted file mode 100644 index 6ed68316eb3f65dec9063332d2f69bf3093bbfab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc diff --git a/coverage/lcov-report/sorter.js b/coverage/lcov-report/sorter.js deleted file mode 100644 index 4ed70ae..0000000 --- a/coverage/lcov-report/sorter.js +++ /dev/null @@ -1,210 +0,0 @@ -/* eslint-disable */ -var addSorting = (function() { - 'use strict'; - var cols, - currentSort = { - index: 0, - desc: false - }; - - // returns the summary table element - function getTable() { - return document.querySelector('.coverage-summary'); - } - // returns the thead element of the summary table - function getTableHeader() { - return getTable().querySelector('thead tr'); - } - // returns the tbody element of the summary table - function getTableBody() { - return getTable().querySelector('tbody'); - } - // returns the th element for nth column - function getNthColumn(n) { - return getTableHeader().querySelectorAll('th')[n]; - } - - function onFilterInput() { - const searchValue = document.getElementById('fileSearch').value; - const rows = document.getElementsByTagName('tbody')[0].children; - - // Try to create a RegExp from the searchValue. If it fails (invalid regex), - // it will be treated as a plain text search - let searchRegex; - try { - searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive - } catch (error) { - searchRegex = null; - } - - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - let isMatch = false; - - if (searchRegex) { - // If a valid regex was created, use it for matching - isMatch = searchRegex.test(row.textContent); - } else { - // Otherwise, fall back to the original plain text search - isMatch = row.textContent - .toLowerCase() - .includes(searchValue.toLowerCase()); - } - - row.style.display = isMatch ? '' : 'none'; - } - } - - // loads the search box - function addSearchBox() { - var template = document.getElementById('filterTemplate'); - var templateClone = template.content.cloneNode(true); - templateClone.getElementById('fileSearch').oninput = onFilterInput; - template.parentElement.appendChild(templateClone); - } - - // loads all columns - function loadColumns() { - var colNodes = getTableHeader().querySelectorAll('th'), - colNode, - cols = [], - col, - i; - - for (i = 0; i < colNodes.length; i += 1) { - colNode = colNodes[i]; - col = { - key: colNode.getAttribute('data-col'), - sortable: !colNode.getAttribute('data-nosort'), - type: colNode.getAttribute('data-type') || 'string' - }; - cols.push(col); - if (col.sortable) { - col.defaultDescSort = col.type === 'number'; - colNode.innerHTML = - colNode.innerHTML + ''; - } - } - return cols; - } - // attaches a data attribute to every tr element with an object - // of data values keyed by column name - function loadRowData(tableRow) { - var tableCols = tableRow.querySelectorAll('td'), - colNode, - col, - data = {}, - i, - val; - for (i = 0; i < tableCols.length; i += 1) { - colNode = tableCols[i]; - col = cols[i]; - val = colNode.getAttribute('data-value'); - if (col.type === 'number') { - val = Number(val); - } - data[col.key] = val; - } - return data; - } - // loads all row data - function loadData() { - var rows = getTableBody().querySelectorAll('tr'), - i; - - for (i = 0; i < rows.length; i += 1) { - rows[i].data = loadRowData(rows[i]); - } - } - // sorts the table using the data for the ith column - function sortByIndex(index, desc) { - var key = cols[index].key, - sorter = function(a, b) { - a = a.data[key]; - b = b.data[key]; - return a < b ? -1 : a > b ? 1 : 0; - }, - finalSorter = sorter, - tableBody = document.querySelector('.coverage-summary tbody'), - rowNodes = tableBody.querySelectorAll('tr'), - rows = [], - i; - - if (desc) { - finalSorter = function(a, b) { - return -1 * sorter(a, b); - }; - } - - for (i = 0; i < rowNodes.length; i += 1) { - rows.push(rowNodes[i]); - tableBody.removeChild(rowNodes[i]); - } - - rows.sort(finalSorter); - - for (i = 0; i < rows.length; i += 1) { - tableBody.appendChild(rows[i]); - } - } - // removes sort indicators for current column being sorted - function removeSortIndicators() { - var col = getNthColumn(currentSort.index), - cls = col.className; - - cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); - col.className = cls; - } - // adds sort indicators for current column being sorted - function addSortIndicators() { - getNthColumn(currentSort.index).className += currentSort.desc - ? ' sorted-desc' - : ' sorted'; - } - // adds event listeners for all sorter widgets - function enableUI() { - var i, - el, - ithSorter = function ithSorter(i) { - var col = cols[i]; - - return function() { - var desc = col.defaultDescSort; - - if (currentSort.index === i) { - desc = !currentSort.desc; - } - sortByIndex(i, desc); - removeSortIndicators(); - currentSort.index = i; - currentSort.desc = desc; - addSortIndicators(); - }; - }; - for (i = 0; i < cols.length; i += 1) { - if (cols[i].sortable) { - // add the click event handler on the th so users - // dont have to click on those tiny arrows - el = getNthColumn(i).querySelector('.sorter').parentElement; - if (el.addEventListener) { - el.addEventListener('click', ithSorter(i)); - } else { - el.attachEvent('onclick', ithSorter(i)); - } - } - } - } - // adds sorting functionality to the UI - return function() { - if (!getTable()) { - return; - } - cols = loadColumns(); - loadData(); - addSearchBox(); - addSortIndicators(); - enableUI(); - }; -})(); - -window.addEventListener('load', addSorting); diff --git a/coverage/lcov-report/tool-executor.ts.html b/coverage/lcov-report/tool-executor.ts.html deleted file mode 100644 index dc2d5d4..0000000 --- a/coverage/lcov-report/tool-executor.ts.html +++ /dev/null @@ -1,478 +0,0 @@ - - - - - - Code coverage report for tool-executor.ts - - - - - - - - - -
-
-

All files tool-executor.ts

-
- -
- 91.37% - Statements - 53/58 -
- - -
- 78.78% - Branches - 26/33 -
- - -
- 100% - Functions - 4/4 -
- - -
- 91.37% - Lines - 53/58 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132  -  -  -  -2x -  -  -2x -2x -2x -  -2x -  -  -  -  -63x -63x -  -  -  -  -23x -1x -  -  -  -22x -1x -  -  -  -21x -8x -  -  -  -13x -3x -  -  -  -10x -  -  -  -  -10x -1x -  -  -  -9x -9x -1x -  -  -  -8x -16x -  -  -16x -  -  -  -  -8x -  -  -  -28x -28x -28x -  -28x -  -  -  -  -  -28x -27x -27x -  -1x -  -1x -1x -  -  -  -  -  -27x -  -26x -  -1x -  -  -20x -20x -  -  -  -  -26x -26x -  -26x -2x -  -  -24x -1x -  -  -23x -15x -  -  -8x -8x -7x -  -1x -1x -  -  -  - 
// src/tool-executor.ts
- 
-import { Vault, App } from 'obsidian';
-import type { ToolCall, ToolResult } from './types';
-import { safeParseJson } from './utils';
- 
-// Disallow characters that are invalid in file paths
-const INVALID_PATH_CHARS = /[<>:"|?*~]/;
-const MAX_PATH_LENGTH = 200;
-const FORBIDDEN_DIRS = ['.obsidian', '.git'];
- 
-export class ToolExecutor {
-  private vault: Vault;
-  private app: App;
- 
-  constructor(vault: Vault, app: App) {
-    this.vault = vault;
-    this.app = app;
-  }
- 
-  private isSafePath(path: string): boolean {
-    // 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:)
-    Iif (/^[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) {
-      Iif (normalized.startsWith(`${dir}/`) || normalized.startsWith(`${dir}\\`)) {
-        return false;
-      }
-      Iif (normalized.includes(`/${dir}/`) || normalized.includes(`\\${dir}\\`)) {
-        return false;
-      }
-    }
- 
-    return true;
-  }
- 
-  async handleToolCall(toolCall: ToolCall): Promise<ToolResult> {
-    try {
-      const toolName = toolCall.function?.name;
-      const rawArgs = toolCall.function?.arguments;
- 
-      Iif (!toolName) {
-        throw new Error('Tool name is required');
-      }
- 
-      // Parse arguments whether they're a string or object
-      let parsedArgs: Record<string, unknown>;
-      if (typeof rawArgs === 'string') {
-        try {
-          parsedArgs = safeParseJson(rawArgs) as Record<string, unknown>;
-        } catch {
-          throw new Error('Invalid JSON arguments');
-        }
-      } else if (rawArgs && typeof rawArgs === 'object') {
-        parsedArgs = rawArgs;
-      } else E{
-        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);
-        default:
-          return { success: false, message: `Unknown tool: ${toolName}` };
-      }
-    } catch (error) {
-      const errorMessage = error instanceof Error ? error.message : String(error);
-      throw new Error(errorMessage);
-    }
-  }
- 
-  private async handleCreateFile(args: Record<string, unknown>): Promise<ToolResult> {
-    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);
-    }
-  }
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/types.ts.html b/coverage/lcov-report/types.ts.html deleted file mode 100644 index 1dee745..0000000 --- a/coverage/lcov-report/types.ts.html +++ /dev/null @@ -1,619 +0,0 @@ - - - - - - Code coverage report for types.ts - - - - - - - - - -
-
-

All files types.ts

-
- -
- 100% - Statements - 35/35 -
- - -
- 100% - Branches - 2/2 -
- - -
- 100% - Functions - 8/8 -
- - -
- 100% - Lines - 35/35 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179  -  -  -  -  -  -3x -3x -3x -3x -3x -3x -3x -3x -  -  -3x -  -  -  -19x -19x -19x -  -  -  -3x -  -  -  -4x -4x -4x -  -  -  -3x -  -  -  -5x -5x -5x -  -  -  -  -  -  -  -  -3x -  -  -  -3x -3x -3x -  -  -  -3x -  -2x -2x -  -  -  -3x -  -  -  -2x -2x -2x -  -  -  -3x -  -  -  -2x -2x -2x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
// src/types.ts
- 
-// ============================================================
-// Error Type Hierarchy
-// ============================================================
- 
-export enum ErrorType {
-  NETWORK_ERROR = 'network_error',
-  API_ERROR = 'api_error',
-  VALIDATION_ERROR = 'validation_error',
-  STREAMING_ERROR = 'streaming_error',
-  TOOL_EXECUTION_ERROR = 'tool_execution_error',
-  PATH_VALIDATION_ERROR = 'path_validation_error',
-  UNKNOWN_ERROR = 'unknown_error',
-}
- 
-export class OllamaError extends Error {
-  public readonly type: ErrorType;
- 
-  constructor(message: string, type: ErrorType) {
-    super(message);
-    this.type = type;
-    Object.setPrototypeOf(this, OllamaError.prototype);
-  }
-}
- 
-export class NetworkError extends OllamaError {
-  public readonly statusCode?: number;
- 
-  constructor(message: string, statusCode?: number) {
-    super(message, ErrorType.NETWORK_ERROR);
-    this.statusCode = statusCode;
-    Object.setPrototypeOf(this, NetworkError.prototype);
-  }
-}
- 
-export class ApiError extends OllamaError {
-  public readonly statusCode?: number;
- 
-  constructor(message: string, statusCode?: number) {
-    super(message, ErrorType.API_ERROR);
-    this.statusCode = statusCode;
-    Object.setPrototypeOf(this, ApiError.prototype);
-  }
-}
- 
-export interface ValidationFieldDetails {
-  field?: string;
-  message?: string;
-}
- 
-export class ValidationError extends OllamaError {
-  public readonly details?: ValidationFieldDetails;
- 
-  constructor(message: string, details?: ValidationFieldDetails) {
-    super(message, ErrorType.VALIDATION_ERROR);
-    this.details = details;
-    Object.setPrototypeOf(this, ValidationError.prototype);
-  }
-}
- 
-export class StreamingError extends OllamaError {
-  constructor(message: string) {
-    super(message, ErrorType.STREAMING_ERROR);
-    Object.setPrototypeOf(this, StreamingError.prototype);
-  }
-}
- 
-export class ToolExecutionError extends OllamaError {
-  public readonly toolName: string;
- 
-  constructor(message: string, toolName: string) {
-    super(message, ErrorType.TOOL_EXECUTION_ERROR);
-    this.toolName = toolName;
-    Object.setPrototypeOf(this, ToolExecutionError.prototype);
-  }
-}
- 
-export class PathValidationError extends OllamaError {
-  public readonly path: string;
- 
-  constructor(message: string, path: string) {
-    super(message, ErrorType.PATH_VALIDATION_ERROR);
-    this.path = path;
-    Object.setPrototypeOf(this, PathValidationError.prototype);
-  }
-}
- 
-// ============================================================
-// Plugin Configuration
-// ============================================================
- 
-export interface PluginSettings {
-  ollamaUrl: string;
-  model: string;
-  vaultSearchLimit: number;
-  maxMessageHistory: number;
-  lastIndexTime: number;
-}
- 
-// ============================================================
-// Ollama Protocol Types
-// ============================================================
- 
-export interface OllamaTool {
-  type: 'function';
-  function: {
-    name: string;
-    description: string;
-    parameters: {
-      type: 'object';
-      properties: Record<string, unknown>;
-      required: string[];
-    };
-  };
-}
- 
-export interface OllamaToolCall {
-  id: string;
-  type: 'function';
-  function: {
-    name: string;
-    arguments: string | Record<string, unknown>;
-  };
-}
- 
-export interface OllamaMessage {
-  role: 'system' | 'user' | 'assistant' | 'tool';
-  content: string;
-  tool_calls?: OllamaToolCall[];
-}
- 
-// ============================================================
-// Tool Execution Types
-// ============================================================
- 
-export interface ToolCall {
-  id: string;
-  type: 'function';
-  function: {
-    name: string;
-    arguments: string | Record<string, unknown>;
-  };
-}
- 
-export interface ToolResult {
-  success: boolean;
-  message: string;
-}
- 
-export interface ExecutionResult {
-  success: boolean;
-  output: string;
-}
- 
-// ============================================================
-// Chat Message Types
-// ============================================================
- 
-export interface ChatMessage {
-  id: string;
-  role: 'user' | 'assistant';
-  content: string;
-  timestamp: number;
-  isStreaming?: boolean;
-  tool_calls?: ToolCall[];
-}
- 
-// ============================================================
-// Vault Index Types
-// ============================================================
- 
-export interface VaultIndexEntry {
-  path: string;
-  title: string;
-  content: string;
-  score: number;
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/utils.ts.html b/coverage/lcov-report/utils.ts.html deleted file mode 100644 index d787644..0000000 --- a/coverage/lcov-report/utils.ts.html +++ /dev/null @@ -1,658 +0,0 @@ - - - - - - Code coverage report for utils.ts - - - - - - - - - -
-
-

All files utils.ts

-
- -
- 95.65% - Statements - 88/92 -
- - -
- 84.78% - Branches - 39/46 -
- - -
- 93.33% - Functions - 14/15 -
- - -
- 96.59% - Lines - 85/88 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -1926x -6x -6x -6x -6x -  -  -6x -  -  -  -  -  -  -6x -6x -  -  -23x -11x -11x -  -12x -  -  -  -  -4x -1x -  -  -  -  -4x -2x -  -  -  -  -64x -63x -  -  -  -  -2x -2x -  -  -  -  -  -  -6x -12x -2x -  -  -10x -  -10x -1x -  -  -9x -9x -6x -  -  -6x -  -3x -  -  -  -6x -15x -1x -  -  -14x -  -  -14x -1x -  -  -13x -1x -  -  -12x -1x -  -  -11x -3x -  -  -  -  -  -8x -  -  -6x -4x -  -4x -4x -2x -  -  -4x -4x -2x -  -  -4x -  -  -  -  -6x -6x -  -  -117x -1x -  -116x -  -  -116x -58x -58x -83x -  -58x -  -  -6x -41x -1x -  -  -40x -1x -  -  -  -39x -39x -  -2x -  -  -  -37x -121x -59x -  -  -62x -183x -3x -  -  -  -59x -84x -  -  -  -  -59x -  -  -37x -3x -  -  -  -34x -1x -  -  -33x -  -  -  - 
export enum LogLevel {
-  DEBUG = 0,
-  INFO = 1,
-  WARN = 2,
-  ERROR = 3,
-}
- 
-const SEVERITY_ORDER: Record<string, number> = {
-  debug: LogLevel.DEBUG,
-  info: LogLevel.INFO,
-  warn: LogLevel.WARN,
-  error: LogLevel.ERROR,
-};
- 
-export class Logger {
-  private static minLevel: LogLevel = LogLevel.DEBUG;
- 
-  static setLevel(level: string | LogLevel): void {
-    if (typeof level === 'string') {
-      const lowerLevel = level.toLowerCase();
-      Logger.minLevel = SEVERITY_ORDER[lowerLevel] ?? LogLevel.DEBUG;
-    } else {
-      Logger.minLevel = level;
-    }
-  }
- 
-  static debug(message: string, category: string = 'general'): void {
-    if (LogLevel.DEBUG >= Logger.minLevel) {
-      console.debug(`[${category}] DEBUG: ${message}`);
-    }
-  }
- 
-  static info(message: string, category: string = 'general'): void {
-    if (LogLevel.INFO >= Logger.minLevel) {
-      console.info(`[${category}] INFO: ${message}`);
-    }
-  }
- 
-  static warn(message: string, category: string = 'general'): void {
-    if (LogLevel.WARN >= Logger.minLevel) {
-      console.warn(`[${category}] WARN: ${message}`);
-    }
-  }
- 
-  static error(message: string, category: string = 'general'): void {
-    if (LogLevel.ERROR >= Logger.minLevel) {
-      console.error(`[${category}] ERROR: ${message}`);
-    }
-  }
-}
- 
-// ==================== URL & Model Validation ====================
- 
-export function validateOllamaUrl(url: string): { valid: boolean; error?: string } {
-  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);
-    Iif (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' };
-  }
-}
- 
-export function validateModelName(model: string): { valid: boolean; error?: string } {
-  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 };
-}
- 
-export function validatePluginSettings(settings: { ollamaUrl: string; model: string }): string[] {
-  const errors: string[] = [];
- 
-  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 = 1_000_000;
-const MAX_JSON_NESTING = 24;
- 
-function countNestingDepth(value: unknown, depth: number = 0): number {
-  if (depth > MAX_JSON_NESTING) {
-    return depth;
-  }
-  Iif (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 as Record<string, unknown>);
-    if (entries.length === 0) return depth;
-    return Math.max(...entries.map((item) => countNestingDepth(item, depth + 1)), depth);
-  }
-  return depth;
-}
- 
-export function safeParseJson(jsonString: string): unknown {
-  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: unknown;
-  try {
-    parsed = JSON.parse(jsonString);
-  } catch {
-    throw new Error('Invalid JSON');
-  }
- 
-  // Check for dangerous prototype pollution patterns in object keys only
-  const checkDangerousPatterns = (obj: unknown): boolean => {
-    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
-    for (const key in obj as Record<string, unknown>) {
-      Iif (checkDangerousPatterns((obj as Record<string, unknown>)[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 ====================
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/vault-indexer.ts.html b/coverage/lcov-report/vault-indexer.ts.html deleted file mode 100644 index 08ecc0e..0000000 --- a/coverage/lcov-report/vault-indexer.ts.html +++ /dev/null @@ -1,1198 +0,0 @@ - - - - - - Code coverage report for vault-indexer.ts - - - - - - - - - -
-
-

All files vault-indexer.ts

-
- -
- 88.51% - Statements - 131/148 -
- - -
- 77.77% - Branches - 42/54 -
- - -
- 83.33% - Functions - 20/24 -
- - -
- 89.85% - Lines - 124/138 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372  -  -  -3x -  -  -  -  -  -  -  -  -  -  -  -1x -  -  -  -2x -  -  -  -1x -1x -  -  -  -  -  -  -  -  -  -34x -  -  -  -  -  -  -18x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -71x -  -  -  -71x -71x -  -  -  -38x -3x -  -  -35x -  -  -  -35x -35x -  -3x -3x -  -  -  -  -3x -1x -1x -1x -  -  -  -  -  -  -34x -34x -34x -34x -  -34x -53x -38x -  -  -34x -2x -2x -  -  -  -  -  -  -  -  -34x -  -  -  -  -  -  -  -34x -34x -34x -34x -  -  -  -34x -  -  -  -  -  -34x -34x -18x -  -  -  -18x -18x -  -63x -63x -62x -62x -62x -53x -  -  -  -  -  -53x -53x -53x -  -  -  -9x -  -1x -  -  -  -1x -  -  -  -  -18x -63x -  -18x -  -  -  -  -  -  -  -34x -  -  -34x -  -  -  -112x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -112x -  -  -469x -  -  -  -69x -69x -69x -  -  -69x -69x -2x -2x -2x -2x -3x -3x -3x -3x -1x -1x -  -2x -2x -2x -  -  -  -  -  -  -  -  -69x -69x -12x -  -  -69x -69x -69x -  -  -69x -  -  -  -  -  -69x -  -69x -  -  -  -  -  -  -  -67x -67x -  -67x -73x -73x -73x -  -73x -  -  -  -  -  -73x -  -  -  -  -1x -1x -  -  -73x -1x -1x -  -  -73x -4x -4x -  -  -73x -58x -58x -  -  -73x -56x -56x -  -  -73x -63x -63x -  -  -  -67x -  -  -  -  -  -  -  -145x -145x -144x -144x -122x -  -  -  -72x -72x -  -  -  -3x -  -  -3x -  -  - 
// src/vault-indexer.ts
- 
-import { VaultIndexEntry } from './types';
-import { Logger } from './utils';
- 
-interface Cache {
-  get(key: string): Promise<string | null>;
-  put(key: string, value: string): Promise<void>;
-  clear(): Promise<void>;
-}
- 
-class InMemoryCache implements Cache {
-  private store: Map<string, string>;
- 
-  constructor() {
-    this.store = new Map();
-  }
- 
-  get(key: string): Promise<string | null> {
-    return Promise.resolve(this.store.get(key) || null);
-  }
- 
-  put(key: string, value: string): Promise<void> {
-    this.store.set(key, value);
-    return Promise.resolve();
-  }
- 
-  clear(): Promise<void> {
-    this.store.clear();
-    return Promise.resolve();
-  }
-}
- 
-class CancellationToken {
-  private cancelled = false;
- 
-  cancel(): void {
-    this.cancelled = true;
-  }
- 
-  get isCancelled(): boolean {
-    return this.cancelled;
-  }
-}
- 
-interface Frontmatter {
-  title?: string;
-  tags?: string;
-}
- 
-interface VaultFile {
-  basename: string;
-  path: string;
-}
- 
-interface VaultLike {
-  getMarkdownFiles(): VaultFile[];
-  read(file: VaultFile): Promise<string>;
-}
- 
-interface TokenizedContent {
-  tokens: string[];
-  headings: string[];
-  frontmatter: Frontmatter;
-  firstParagraph?: string;
-}
- 
-interface ScoreResult {
-  score: number;
-  matchedFields: string[];
-}
- 
-class VaultIndexer {
-  private vault: VaultLike | null = null;
-  private cache?: Cache;
- 
-  constructor(vault: VaultLike, cache?: Cache) {
-    this.vault = vault;
-    this.cache = cache;
-  }
- 
-  async searchVault(query: string, limit: number = 5): Promise<VaultIndexEntry[]> {
-    if (!query || !query.trim()) {
-      return [];
-    }
- 
-    Iif (!this.vault) {
-      throw new Error('Vault-like object not provided to VaultIndexer');
-    }
- 
-    const cacheKey = `query:${query.trim()}:limit:${limit}`;
-    if (this.cache) {
-      let cachedResults;
-      try {
-        cachedResults = await this.cache.get(cacheKey);
-      } catch {
-        // Ignore cache retrieval errors and continue with normal processing
-        cachedResults = null;
-      }
-      if (cachedResults) {
-        try {
-          const parsedResults = JSON.parse(cachedResults) as VaultIndexEntry[];
-          return parsedResults.slice(0, limit);
-        } catch {
-          // Ignore cache parse errors and continue with normal processing
-        }
-      }
-    }
- 
-    const queryTokens = this.tokenize(query.trim());
-    const vault = this.vault;
-    const allFiles = vault.getMarkdownFiles();
-    const results = await this.processFilesInBatches(vault, allFiles, queryTokens);
- 
-    const filteredResults = results
-      .filter((result): result is NonNullable<typeof result> => result !== null)
-      .sort((a, b) => b.score - a.score)
-      .slice(0, limit);
- 
-    if (this.cache) {
-      try {
-        await this.cache.put(cacheKey, JSON.stringify(filteredResults));
-      } catch (error) {
-        Logger.warn(
-          `Failed to cache results for query "${query}": ${error instanceof Error ? error.message : String(error)}`,
-          'vault-indexer'
-        );
-      }
-    }
- 
-    return filteredResults;
-  }
- 
-  private async processFilesInBatches(
-    vault: VaultLike,
-    files: VaultFile[],
-    queryTokens: string[]
-  ): Promise<Array<VaultIndexEntry | null>> {
-    const batchSize = 10;
-    const results: VaultIndexEntry[] = [];
-    const seenPaths = new Set<string>();
-    const cancellationToken = new CancellationToken();
-    // Removed unused processedCount variable
- 
-    // Set up a check for cancellation every 100 files
-    const checkInterval = setInterval(() => {
-      Iif (cancellationToken.isCancelled) {
-        clearInterval(checkInterval);
-      }
-    }, 100);
- 
-    try {
-      for (let i = 0; i < files.length; i += batchSize) {
-        Iif (cancellationToken.isCancelled) {
-          break;
-        }
- 
-        const batch = files.slice(i, i + batchSize);
-        const batchResults = await Promise.all(
-          batch.map(async (file) => {
-            try {
-              const content = await vault.read(file);
-              const tokenized = this.tokenizeContent(content);
-              const scoreResult = this.calculateWeightedScore(tokenized, queryTokens, file);
-              if (scoreResult.score > 0) {
-                const entry: VaultIndexEntry = {
-                  path: file.path,
-                  title: file.basename.replace(/\.md$/, ''),
-                  content: content.substring(0, 500),
-                  score: scoreResult.score,
-                };
-                if (!seenPaths.has(entry.path)) {
-                  seenPaths.add(entry.path);
-                  return entry;
-                }
-                return null;
-              }
-              return null;
-            } catch (error) {
-              Logger.warn(
-                `Failed to process ${file.path}: ${error instanceof Error ? error.message : String(error)}`,
-                'vault-indexer'
-              );
-              return null;
-            }
-          })
-        );
- 
-        const validResults = batchResults.filter(
-          (result): result is NonNullable<typeof result> => result !== null
-        );
-        results.push(...validResults);
- 
-        // Continue processing all files to ensure we don't miss higher-scoring results
-        // even if we've already found some matches
- 
-        // Removed processedCount increment
-      }
-    } finally {
-      clearInterval(checkInterval);
-    }
- 
-    return results;
-  }
- 
-  private tokenize(text: string): string[] {
-    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',
-    ]);
-    return text
-      .toLowerCase()
-      .split(/\W+/)
-      .filter((token) => token.length > 1 && !stopWords.has(token));
-  }
- 
-  private tokenizeContent(content: string): TokenizedContent {
-    const tokens: string[] = [];
-    const headings: string[] = [];
-    const frontmatter: Frontmatter = {};
-    let firstParagraph: string | undefined;
- 
-    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(':');
-          Iif (!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;
-            }
-          }
-        }
-      } catch {
-        Logger.warn('Failed to parse frontmatter', 'vault-indexer');
-      }
-    }
- 
-    const headingMatches = content.match(/^#{1,6} (.*?)$/gm);
-    if (headingMatches) {
-      headings.push(...headingMatches.map((h: string) => h.replace(/^#{1,6} /, '')));
-    }
- 
-    const paragraphMatch = content.match(/^([^#]*?)(?=#|\n\n|$)/s);
-    if (paragraphMatch) {
-      firstParagraph = paragraphMatch[1].trim();
-    }
- 
-    const allText = content
-      .replace(/^---.*?---/s, '')
-      .replace(/^#.*?$/gm, '')
-      .replace(/```.*?```/gs, '')
-      .replace(/`.*?`/g, '')
-      .replace(/\[.*?\]\(.*?\)/g, '');
-    tokens.push(...this.tokenize(allText));
- 
-    return { tokens, headings, frontmatter, firstParagraph };
-  }
- 
-  private calculateWeightedScore(
-    tokenized: TokenizedContent,
-    queryTokens: string[],
-    file?: VaultFile
-  ): ScoreResult {
-    let totalScore = 0;
-    const matchedTokens: Set<string> = new Set<string>();
- 
-    for (const queryToken of queryTokens) {
-      let tokenScore = 0;
-      const stemmed = this.stemToken(queryToken);
-      let matched = false;
- 
-      Iif (
-        tokenized.frontmatter?.title &&
-        this.exactMatch(tokenized.frontmatter.title, queryToken)
-      ) {
-        tokenScore += 3;
-        matched = true;
-      } else if (
-        file &&
-        file.basename &&
-        this.exactMatch(file.basename.replace(/\.md$/, ''), queryToken)
-      ) {
-        tokenScore += 3;
-        matched = true;
-      }
- 
-      if (tokenized.frontmatter?.tags && this.exactMatch(tokenized.frontmatter.tags, queryToken)) {
-        tokenScore += 2.5;
-        matched = true;
-      }
- 
-      if (tokenized.headings.some((heading: string) => heading.toLowerCase().includes(stemmed))) {
-        tokenScore += 5;
-        matched = true;
-      }
- 
-      if (tokenized.firstParagraph && tokenized.firstParagraph.toLowerCase().includes(stemmed)) {
-        tokenScore += 1.5;
-        matched = true;
-      }
- 
-      if (tokenized.tokens.includes(stemmed)) {
-        tokenScore += 1;
-        matched = true;
-      }
- 
-      if (matched) {
-        totalScore += tokenScore;
-        matchedTokens.add(queryToken);
-      }
-    }
- 
-    return {
-      score: totalScore,
-      matchedFields: Array.from(matchedTokens),
-    };
-  }
- 
-  private stemToken(token: string): string {
-    // Improved stemmer that handles edge cases
-    Iif (token.length <= 3) return token; // Don't stem very short tokens
-    if (token.endsWith('s')) return token.slice(0, -1);
-    Iif (token.endsWith('ed') && token.length > 4) return token.slice(0, -2); // Don't stem 3-letter words ending in ed
-    if (token.endsWith('ing') && token.length > 5) return token.slice(0, -3); // Don't stem 4-letter words ending in ing
-    return token;
-  }
- 
-  private exactMatch(content: string, token: string): boolean {
-    const stemmedToken = this.stemToken(token);
-    return content.toLowerCase().includes(stemmedToken);
-  }
-}
- 
-export { VaultIndexer, Cache, InMemoryCache, CancellationToken };
- 
-// Convenience method to create a VaultIndexer with an in-memory cache
-export function createVaultIndexerWithCache(vault: VaultLike): VaultIndexer {
-  return new VaultIndexer(vault, new InMemoryCache());
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov.info b/coverage/lcov.info deleted file mode 100644 index d5ee220..0000000 --- a/coverage/lcov.info +++ /dev/null @@ -1,1289 +0,0 @@ -TN: -SF:src/chat-view.ts -FN:44,(anonymous_0) -FN:48,(anonymous_1) -FN:52,(anonymous_2) -FN:56,(anonymous_3) -FN:64,(anonymous_4) -FN:69,(anonymous_5) -FN:73,(anonymous_6) -FN:77,(anonymous_7) -FN:84,(anonymous_8) -FN:88,(anonymous_9) -FN:99,(anonymous_10) -FN:101,(anonymous_11) -FN:108,(anonymous_12) -FN:141,(anonymous_13) -FN:162,(anonymous_14) -FN:168,(anonymous_15) -FN:179,(anonymous_16) -FN:184,(anonymous_17) -FN:192,(anonymous_18) -FN:201,(anonymous_19) -FN:204,(anonymous_20) -FN:207,(anonymous_21) -FN:216,(anonymous_22) -FN:223,(anonymous_23) -FN:239,(anonymous_24) -FN:247,(anonymous_25) -FN:248,(anonymous_26) -FN:258,(anonymous_27) -FN:259,(anonymous_28) -FN:271,(anonymous_29) -FN:282,(anonymous_30) -FN:305,(anonymous_31) -FN:400,(anonymous_32) -FN:419,(anonymous_33) -FN:467,(anonymous_34) -FNF:35 -FNH:29 -FNDA:2,(anonymous_0) -FNDA:4,(anonymous_1) -FNDA:2,(anonymous_2) -FNDA:36,(anonymous_3) -FNDA:0,(anonymous_4) -FNDA:1,(anonymous_5) -FNDA:1,(anonymous_6) -FNDA:5,(anonymous_7) -FNDA:0,(anonymous_8) -FNDA:3,(anonymous_9) -FNDA:28,(anonymous_10) -FNDA:146,(anonymous_11) -FNDA:43,(anonymous_12) -FNDA:282,(anonymous_13) -FNDA:3661,(anonymous_14) -FNDA:282,(anonymous_15) -FNDA:13,(anonymous_16) -FNDA:2,(anonymous_17) -FNDA:4,(anonymous_18) -FNDA:0,(anonymous_19) -FNDA:0,(anonymous_20) -FNDA:0,(anonymous_21) -FNDA:2,(anonymous_22) -FNDA:8,(anonymous_23) -FNDA:2,(anonymous_24) -FNDA:26,(anonymous_25) -FNDA:292,(anonymous_26) -FNDA:19,(anonymous_27) -FNDA:158,(anonymous_28) -FNDA:21,(anonymous_29) -FNDA:0,(anonymous_30) -FNDA:120,(anonymous_31) -FNDA:6,(anonymous_32) -FNDA:2,(anonymous_33) -FNDA:12,(anonymous_34) -DA:1,1 -DA:8,1 -DA:9,1 -DA:10,1 -DA:19,1 -DA:20,1 -DA:21,1 -DA:22,1 -DA:24,1 -DA:26,36 -DA:30,36 -DA:31,36 -DA:32,36 -DA:33,36 -DA:34,36 -DA:35,36 -DA:36,36 -DA:37,36 -DA:38,36 -DA:39,36 -DA:40,36 -DA:41,36 -DA:45,2 -DA:49,4 -DA:53,2 -DA:57,36 -DA:58,36 -DA:59,36 -DA:60,36 -DA:61,36 -DA:65,0 -DA:66,0 -DA:70,1 -DA:74,1 -DA:78,5 -DA:79,5 -DA:80,5 -DA:81,5 -DA:85,0 -DA:89,3 -DA:90,3 -DA:91,3 -DA:92,3 -DA:93,3 -DA:94,3 -DA:95,3 -DA:96,3 -DA:101,146 -DA:102,28 -DA:103,0 -DA:104,0 -DA:110,43 -DA:111,43 -DA:113,43 -DA:116,43 -DA:117,4 -DA:119,43 -DA:120,4 -DA:123,4 -DA:126,43 -DA:128,27 -DA:130,27 -DA:133,27 -DA:134,27 -DA:138,43 -DA:141,282 -DA:144,43 -DA:146,43 -DA:147,262 -DA:148,262 -DA:149,109 -DA:151,153 -DA:154,153 -DA:155,153 -DA:160,43 -DA:161,133 -DA:162,3661 -DA:163,24 -DA:168,282 -DA:169,43 -DA:170,1 -DA:173,1 -DA:174,1 -DA:180,13 -DA:183,13 -DA:184,13 -DA:185,2 -DA:186,2 -DA:187,2 -DA:191,13 -DA:192,13 -DA:193,4 -DA:194,2 -DA:195,2 -DA:196,2 -DA:201,13 -DA:202,0 -DA:204,13 -DA:205,0 -DA:207,13 -DA:208,0 -DA:212,13 -DA:213,13 -DA:214,13 -DA:215,7 -DA:216,7 -DA:218,7 -DA:220,13 -DA:224,8 -DA:225,2 -DA:227,8 -DA:228,2 -DA:230,8 -DA:231,2 -DA:233,8 -DA:234,8 -DA:235,8 -DA:236,8 -DA:241,2 -DA:242,2 -DA:243,2 -DA:244,2 -DA:248,292 -DA:249,26 -DA:250,26 -DA:255,26 -DA:259,158 -DA:260,19 -DA:261,12 -DA:264,12 -DA:266,19 -DA:267,19 -DA:272,21 -DA:273,21 -DA:275,21 -DA:277,21 -DA:278,21 -DA:281,19 -DA:282,19 -DA:285,19 -DA:286,19 -DA:287,0 -DA:290,19 -DA:293,19 -DA:297,19 -DA:302,19 -DA:306,120 -DA:315,19 -DA:336,19 -DA:338,19 -DA:339,19 -DA:342,19 -DA:349,19 -DA:358,19 -DA:360,19 -DA:361,19 -DA:363,19 -DA:364,19 -DA:365,19 -DA:366,19 -DA:367,19 -DA:368,17 -DA:369,17 -DA:370,0 -DA:373,17 -DA:374,17 -DA:377,17 -DA:378,4 -DA:381,17 -DA:385,13 -DA:391,13 -DA:393,4 -DA:394,4 -DA:395,0 -DA:399,4 -DA:400,6 -DA:403,4 -DA:404,4 -DA:405,6 -DA:406,2 -DA:409,4 -DA:414,4 -DA:416,2 -DA:419,2 -DA:425,2 -DA:426,2 -DA:427,2 -DA:430,2 -DA:437,2 -DA:445,13 -DA:446,9 -DA:452,13 -DA:453,2 -DA:455,13 -DA:458,19 -DA:462,6 -DA:467,6 -DA:468,12 -DA:470,6 -DA:472,21 -DA:473,21 -LF:201 -LH:190 -BRDA:102,0,0,0 -BRDA:102,1,0,28 -BRDA:102,1,1,6 -BRDA:102,1,2,0 -BRDA:110,2,0,43 -BRDA:110,2,1,29 -BRDA:113,3,0,43 -BRDA:113,3,1,30 -BRDA:116,4,0,4 -BRDA:119,5,0,4 -BRDA:126,6,0,27 -BRDA:128,7,0,27 -BRDA:128,7,1,27 -BRDA:148,8,0,109 -BRDA:148,8,1,153 -BRDA:162,9,0,24 -BRDA:162,10,0,133 -BRDA:162,10,1,133 -BRDA:169,11,0,1 -BRDA:169,12,0,43 -BRDA:169,12,1,20 -BRDA:173,13,0,1 -BRDA:180,14,0,0 -BRDA:180,15,0,13 -BRDA:180,15,1,13 -BRDA:180,15,2,13 -BRDA:183,16,0,13 -BRDA:185,17,0,0 -BRDA:191,18,0,13 -BRDA:193,19,0,2 -BRDA:193,20,0,4 -BRDA:193,20,1,4 -BRDA:193,20,2,4 -BRDA:214,21,0,7 -BRDA:215,22,0,7 -BRDA:224,23,0,2 -BRDA:224,24,0,8 -BRDA:224,24,1,8 -BRDA:227,25,0,2 -BRDA:227,26,0,8 -BRDA:227,26,1,8 -BRDA:230,27,0,2 -BRDA:230,28,0,8 -BRDA:230,28,1,7 -BRDA:249,29,0,0 -BRDA:260,30,0,12 -BRDA:260,31,0,19 -BRDA:260,31,1,19 -BRDA:266,32,0,19 -BRDA:272,33,0,0 -BRDA:272,34,0,21 -BRDA:272,34,1,21 -BRDA:278,35,0,2 -BRDA:286,36,0,0 -BRDA:290,37,0,0 -BRDA:290,37,1,19 -BRDA:369,38,0,0 -BRDA:373,39,0,17 -BRDA:377,40,0,4 -BRDA:391,41,0,4 -BRDA:394,42,0,0 -BRDA:405,43,0,2 -BRDA:405,43,1,4 -BRDA:414,44,0,2 -BRDA:414,44,1,2 -BRDA:445,45,0,9 -BRDA:452,46,0,2 -BRDA:468,47,0,6 -BRDA:468,47,1,6 -BRDA:472,48,0,21 -BRF:70 -BRH:60 -end_of_record -TN: -SF:src/error-handler.ts -FN:16,(anonymous_0) -FN:33,(anonymous_1) -FN:45,(anonymous_2) -FN:66,(anonymous_3) -FN:74,(anonymous_4) -FN:107,(anonymous_5) -FN:111,(anonymous_6) -FN:115,(anonymous_7) -FN:120,(anonymous_8) -FN:124,(anonymous_9) -FN:128,(anonymous_10) -FN:132,(anonymous_11) -FNF:12 -FNH:12 -FNDA:9,(anonymous_0) -FNDA:15,(anonymous_1) -FNDA:7,(anonymous_2) -FNDA:2,(anonymous_3) -FNDA:13,(anonymous_4) -FNDA:1,(anonymous_5) -FNDA:1,(anonymous_6) -FNDA:1,(anonymous_7) -FNDA:1,(anonymous_8) -FNDA:1,(anonymous_9) -FNDA:1,(anonymous_10) -FNDA:1,(anonymous_11) -DA:3,2 -DA:4,2 -DA:15,2 -DA:17,9 -DA:18,9 -DA:20,9 -DA:21,8 -DA:23,8 -DA:24,8 -DA:25,8 -DA:28,1 -DA:29,1 -DA:34,15 -DA:35,7 -DA:38,8 -DA:39,7 -DA:42,1 -DA:46,7 -DA:48,2 -DA:50,0 -DA:52,2 -DA:54,1 -DA:56,1 -DA:58,1 -DA:60,0 -DA:62,0 -DA:67,2 -DA:68,1 -DA:69,1 -DA:71,1 -DA:75,13 -DA:78,13 -DA:79,2 -DA:82,11 -DA:83,2 -DA:86,9 -DA:87,1 -DA:90,8 -DA:91,1 -DA:94,7 -DA:95,1 -DA:98,6 -DA:99,1 -DA:102,5 -DA:108,1 -DA:112,1 -DA:116,1 -DA:117,1 -DA:121,1 -DA:125,1 -DA:129,1 -DA:133,1 -LF:52 -LH:49 -BRDA:20,0,0,8 -BRDA:20,0,1,1 -BRDA:21,1,0,5 -BRDA:21,1,1,3 -BRDA:24,2,0,8 -BRDA:28,3,0,0 -BRDA:28,3,1,1 -BRDA:34,4,0,7 -BRDA:38,5,0,7 -BRDA:46,6,0,2 -BRDA:46,6,1,0 -BRDA:46,6,2,2 -BRDA:46,6,3,1 -BRDA:46,6,4,1 -BRDA:46,6,5,1 -BRDA:46,6,6,0 -BRDA:46,6,7,0 -BRDA:67,7,0,1 -BRDA:67,8,0,2 -BRDA:67,8,1,2 -BRDA:69,9,0,1 -BRDA:69,9,1,0 -BRDA:78,10,0,2 -BRDA:78,11,0,13 -BRDA:78,11,1,11 -BRDA:78,11,2,11 -BRDA:82,12,0,2 -BRDA:82,13,0,11 -BRDA:82,13,1,9 -BRDA:82,13,2,9 -BRDA:86,14,0,1 -BRDA:86,15,0,9 -BRDA:86,15,1,8 -BRDA:90,16,0,1 -BRDA:90,17,0,8 -BRDA:90,17,1,7 -BRDA:94,18,0,1 -BRDA:94,19,0,7 -BRDA:94,19,1,6 -BRDA:98,20,0,1 -BRDA:98,21,0,6 -BRDA:98,21,1,5 -BRDA:116,22,0,1 -BRDA:116,22,1,0 -BRDA:125,23,0,1 -BRDA:125,23,1,1 -BRDA:129,24,0,1 -BRDA:129,24,1,1 -BRF:48 -BRH:42 -end_of_record -TN: -SF:src/ollama-client.ts -FN:18,(anonymous_0) -FN:24,(anonymous_1) -FN:31,(anonymous_2) -FN:38,(anonymous_3) -FN:49,(anonymous_4) -FN:81,(anonymous_5) -FN:82,(anonymous_6) -FN:91,(anonymous_7) -FN:92,(anonymous_8) -FN:197,(anonymous_9) -FN:201,(anonymous_10) -FN:231,(anonymous_11) -FN:232,(anonymous_12) -FN:241,(anonymous_13) -FN:242,(anonymous_14) -FN:269,(anonymous_15) -FN:275,(anonymous_16) -FNF:17 -FNH:15 -FNDA:61,(anonymous_0) -FNDA:6,(anonymous_1) -FNDA:24,(anonymous_2) -FNDA:3,(anonymous_3) -FNDA:29,(anonymous_4) -FNDA:4,(anonymous_5) -FNDA:0,(anonymous_6) -FNDA:4,(anonymous_7) -FNDA:4,(anonymous_8) -FNDA:5,(anonymous_9) -FNDA:8,(anonymous_10) -FNDA:2,(anonymous_11) -FNDA:0,(anonymous_12) -FNDA:2,(anonymous_13) -FNDA:2,(anonymous_14) -FNDA:16,(anonymous_15) -FNDA:19,(anonymous_16) -DA:4,2 -DA:5,2 -DA:11,2 -DA:15,61 -DA:16,61 -DA:19,61 -DA:20,61 -DA:21,61 -DA:25,6 -DA:26,2 -DA:27,2 -DA:35,24 -DA:42,3 -DA:43,3 -DA:44,0 -DA:46,0 -DA:55,29 -DA:56,29 -DA:58,29 -DA:59,29 -DA:73,27 -DA:74,8 -DA:75,5 -DA:76,5 -DA:80,5 -DA:81,4 -DA:82,4 -DA:83,0 -DA:85,4 -DA:86,4 -DA:87,4 -DA:88,4 -DA:89,4 -DA:92,4 -DA:98,4 -DA:101,0 -DA:104,5 -DA:105,2 -DA:107,3 -DA:110,15 -DA:111,1 -DA:114,14 -DA:115,14 -DA:116,1 -DA:119,13 -DA:120,13 -DA:121,13 -DA:122,13 -DA:123,13 -DA:125,13 -DA:126,13 -DA:127,22 -DA:128,22 -DA:130,11 -DA:132,11 -DA:133,11 -DA:135,11 -DA:136,69 -DA:138,68 -DA:139,68 -DA:140,15 -DA:142,14 -DA:143,14 -DA:144,0 -DA:147,14 -DA:148,14 -DA:150,54 -DA:151,1 -DA:154,53 -DA:155,53 -DA:156,1 -DA:159,52 -DA:167,11 -DA:168,1 -DA:169,1 -DA:170,1 -DA:172,1 -DA:173,1 -DA:174,1 -DA:177,0 -DA:178,0 -DA:180,0 -DA:187,13 -DA:191,29 -DA:194,11 -DA:198,5 -DA:207,8 -DA:208,8 -DA:209,8 -DA:223,8 -DA:224,4 -DA:225,3 -DA:226,3 -DA:230,3 -DA:231,2 -DA:232,2 -DA:233,0 -DA:235,2 -DA:236,2 -DA:237,2 -DA:238,2 -DA:239,2 -DA:242,2 -DA:248,2 -DA:251,0 -DA:254,3 -DA:256,1 -DA:259,4 -DA:260,4 -DA:265,8 -DA:270,16 -DA:271,1 -DA:276,19 -DA:277,1 -DA:280,18 -DA:281,18 -LF:116 -LH:106 -BRDA:21,0,0,61 -BRDA:21,0,1,36 -BRDA:25,1,0,2 -BRDA:33,2,0,0 -BRDA:40,3,0,0 -BRDA:51,4,0,0 -BRDA:52,5,0,0 -BRDA:73,6,0,8 -BRDA:74,7,0,5 -BRDA:74,8,0,8 -BRDA:74,8,1,6 -BRDA:80,9,0,4 -BRDA:86,10,0,4 -BRDA:86,10,1,0 -BRDA:110,11,0,1 -BRDA:115,12,0,1 -BRDA:115,13,0,14 -BRDA:115,13,1,14 -BRDA:115,13,2,1 -BRDA:128,14,0,11 -BRDA:133,15,0,11 -BRDA:133,15,1,0 -BRDA:136,16,0,1 -BRDA:143,17,0,0 -BRDA:150,18,0,1 -BRDA:150,19,0,54 -BRDA:150,19,1,54 -BRDA:155,20,0,1 -BRDA:167,21,0,1 -BRDA:173,22,0,1 -BRDA:177,23,0,0 -BRDA:177,24,0,0 -BRDA:177,24,1,0 -BRDA:197,25,0,0 -BRDA:203,26,0,0 -BRDA:204,27,0,0 -BRDA:223,28,0,4 -BRDA:224,29,0,3 -BRDA:224,30,0,4 -BRDA:224,30,1,4 -BRDA:230,31,0,2 -BRDA:236,32,0,2 -BRDA:236,32,1,0 -BRDA:261,33,0,4 -BRDA:261,33,1,1 -BRDA:270,34,0,1 -BRDA:276,35,0,1 -BRDA:276,36,0,19 -BRDA:276,36,1,18 -BRDA:282,37,0,18 -BRDA:282,37,1,18 -BRDA:283,38,0,18 -BRDA:283,38,1,0 -BRDA:284,39,0,18 -BRDA:284,39,1,16 -BRF:55 -BRH:40 -end_of_record -TN: -SF:src/tool-executor.ts -FN:16,(anonymous_0) -FN:21,(anonymous_1) -FN:71,(anonymous_2) -FN:107,(anonymous_3) -FNF:4 -FNH:4 -FNDA:63,(anonymous_0) -FNDA:23,(anonymous_1) -FNDA:28,(anonymous_2) -FNDA:26,(anonymous_3) -DA:5,2 -DA:8,2 -DA:9,2 -DA:10,2 -DA:12,2 -DA:17,63 -DA:18,63 -DA:23,23 -DA:24,1 -DA:28,22 -DA:29,1 -DA:33,21 -DA:34,8 -DA:38,13 -DA:39,3 -DA:43,10 -DA:44,0 -DA:48,10 -DA:49,1 -DA:53,9 -DA:54,9 -DA:55,1 -DA:59,8 -DA:60,16 -DA:61,0 -DA:63,16 -DA:64,0 -DA:68,8 -DA:72,28 -DA:73,28 -DA:74,28 -DA:76,28 -DA:77,0 -DA:82,28 -DA:83,27 -DA:84,27 -DA:86,1 -DA:88,1 -DA:89,1 -DA:91,0 -DA:95,27 -DA:97,26 -DA:99,1 -DA:102,20 -DA:103,20 -DA:108,26 -DA:109,26 -DA:111,26 -DA:112,2 -DA:115,24 -DA:116,1 -DA:119,23 -DA:120,15 -DA:123,8 -DA:124,8 -DA:125,7 -DA:127,1 -DA:128,1 -LF:58 -LH:53 -BRDA:23,0,0,1 -BRDA:23,1,0,23 -BRDA:23,1,1,22 -BRDA:28,2,0,1 -BRDA:33,3,0,8 -BRDA:38,4,0,3 -BRDA:38,5,0,13 -BRDA:38,5,1,11 -BRDA:43,6,0,0 -BRDA:48,7,0,1 -BRDA:54,8,0,1 -BRDA:60,9,0,0 -BRDA:60,10,0,16 -BRDA:60,10,1,16 -BRDA:63,11,0,0 -BRDA:63,12,0,16 -BRDA:63,12,1,16 -BRDA:76,13,0,0 -BRDA:82,14,0,27 -BRDA:82,14,1,1 -BRDA:88,15,0,1 -BRDA:88,15,1,0 -BRDA:88,16,0,1 -BRDA:88,16,1,1 -BRDA:95,17,0,26 -BRDA:95,17,1,1 -BRDA:102,18,0,20 -BRDA:102,18,1,0 -BRDA:111,19,0,2 -BRDA:115,20,0,1 -BRDA:119,21,0,15 -BRDA:127,22,0,1 -BRDA:127,22,1,0 -BRF:33 -BRH:26 -end_of_record -TN: -SF:src/types.ts -FN:7,(anonymous_0) -FN:20,(anonymous_1) -FN:30,(anonymous_2) -FN:40,(anonymous_3) -FN:55,(anonymous_4) -FN:63,(anonymous_5) -FN:72,(anonymous_6) -FN:82,(anonymous_7) -FNF:8 -FNH:8 -FNDA:3,(anonymous_0) -FNDA:19,(anonymous_1) -FNDA:4,(anonymous_2) -FNDA:5,(anonymous_3) -FNDA:3,(anonymous_4) -FNDA:2,(anonymous_5) -FNDA:2,(anonymous_6) -FNDA:2,(anonymous_7) -DA:7,3 -DA:8,3 -DA:9,3 -DA:10,3 -DA:11,3 -DA:12,3 -DA:13,3 -DA:14,3 -DA:17,3 -DA:21,19 -DA:22,19 -DA:23,19 -DA:27,3 -DA:31,4 -DA:32,4 -DA:33,4 -DA:37,3 -DA:41,5 -DA:42,5 -DA:43,5 -DA:52,3 -DA:56,3 -DA:57,3 -DA:58,3 -DA:62,3 -DA:64,2 -DA:65,2 -DA:69,3 -DA:73,2 -DA:74,2 -DA:75,2 -DA:79,3 -DA:83,2 -DA:84,2 -DA:85,2 -LF:35 -LH:35 -BRDA:7,0,0,3 -BRDA:7,0,1,3 -BRF:2 -BRH:2 -end_of_record -TN: -SF:src/utils.ts -FN:1,(anonymous_0) -FN:18,(anonymous_1) -FN:27,(anonymous_2) -FN:33,(anonymous_3) -FN:39,(anonymous_4) -FN:45,(anonymous_5) -FN:54,validateOllamaUrl -FN:76,validateModelName -FN:106,validatePluginSettings -FN:127,countNestingDepth -FN:132,(anonymous_10) -FN:137,(anonymous_11) -FN:142,safeParseJson -FN:159,(anonymous_13) -FN:165,(anonymous_14) -FNF:15 -FNH:14 -FNDA:6,(anonymous_0) -FNDA:23,(anonymous_1) -FNDA:4,(anonymous_2) -FNDA:4,(anonymous_3) -FNDA:64,(anonymous_4) -FNDA:2,(anonymous_5) -FNDA:12,validateOllamaUrl -FNDA:15,validateModelName -FNDA:4,validatePluginSettings -FNDA:117,countNestingDepth -FNDA:0,(anonymous_10) -FNDA:83,(anonymous_11) -FNDA:41,safeParseJson -FNDA:121,(anonymous_13) -FNDA:183,(anonymous_14) -DA:1,6 -DA:2,6 -DA:3,6 -DA:4,6 -DA:5,6 -DA:8,6 -DA:15,6 -DA:16,6 -DA:19,23 -DA:20,11 -DA:21,11 -DA:23,12 -DA:28,4 -DA:29,1 -DA:34,4 -DA:35,2 -DA:40,64 -DA:41,63 -DA:46,2 -DA:47,2 -DA:54,6 -DA:55,12 -DA:56,2 -DA:59,10 -DA:61,10 -DA:62,1 -DA:65,9 -DA:66,9 -DA:67,6 -DA:68,0 -DA:70,6 -DA:72,3 -DA:76,6 -DA:77,15 -DA:78,1 -DA:81,14 -DA:84,14 -DA:85,1 -DA:88,13 -DA:89,1 -DA:92,12 -DA:93,1 -DA:96,11 -DA:97,3 -DA:103,8 -DA:106,6 -DA:107,4 -DA:109,4 -DA:110,4 -DA:111,2 -DA:114,4 -DA:115,4 -DA:116,2 -DA:119,4 -DA:124,6 -DA:125,6 -DA:128,117 -DA:129,1 -DA:131,116 -DA:132,0 -DA:134,116 -DA:135,58 -DA:136,58 -DA:137,83 -DA:139,58 -DA:142,6 -DA:143,41 -DA:144,1 -DA:147,40 -DA:148,1 -DA:152,39 -DA:153,39 -DA:155,2 -DA:159,37 -DA:160,121 -DA:161,59 -DA:164,62 -DA:165,183 -DA:166,3 -DA:170,59 -DA:171,84 -DA:172,0 -DA:176,59 -DA:179,37 -DA:180,3 -DA:184,34 -DA:185,1 -DA:188,33 -LF:88 -LH:85 -BRDA:1,0,0,6 -BRDA:1,0,1,6 -BRDA:19,1,0,11 -BRDA:19,1,1,12 -BRDA:21,2,0,11 -BRDA:21,2,1,1 -BRDA:27,3,0,0 -BRDA:28,4,0,1 -BRDA:33,5,0,0 -BRDA:34,6,0,2 -BRDA:39,7,0,0 -BRDA:40,8,0,63 -BRDA:45,9,0,0 -BRDA:46,10,0,2 -BRDA:55,11,0,2 -BRDA:55,12,0,12 -BRDA:55,12,1,11 -BRDA:61,13,0,1 -BRDA:67,14,0,0 -BRDA:67,15,0,6 -BRDA:67,15,1,1 -BRDA:77,16,0,1 -BRDA:84,17,0,1 -BRDA:84,18,0,14 -BRDA:84,18,1,13 -BRDA:88,19,0,1 -BRDA:92,20,0,1 -BRDA:96,21,0,3 -BRDA:110,22,0,2 -BRDA:115,23,0,2 -BRDA:127,24,0,34 -BRDA:128,25,0,1 -BRDA:131,26,0,0 -BRDA:134,27,0,58 -BRDA:134,28,0,116 -BRDA:134,28,1,116 -BRDA:136,29,0,1 -BRDA:143,30,0,1 -BRDA:147,31,0,1 -BRDA:160,32,0,59 -BRDA:160,33,0,121 -BRDA:160,33,1,62 -BRDA:165,34,0,3 -BRDA:171,35,0,0 -BRDA:179,36,0,3 -BRDA:184,37,0,1 -BRF:46 -BRH:39 -end_of_record -TN: -SF:src/vault-indexer.ts -FN:15,(anonymous_0) -FN:19,(anonymous_1) -FN:23,(anonymous_2) -FN:28,(anonymous_3) -FN:34,(anonymous_4) -FN:37,(anonymous_5) -FN:41,(anonymous_6) -FN:77,(anonymous_7) -FN:82,(anonymous_8) -FN:116,(anonymous_9) -FN:117,(anonymous_10) -FN:134,(anonymous_11) -FN:146,(anonymous_12) -FN:160,(anonymous_13) -FN:190,(anonymous_14) -FN:206,(anonymous_15) -FN:237,(anonymous_16) -FN:240,(anonymous_17) -FN:272,(anonymous_18) -FN:291,(anonymous_19) -FN:324,(anonymous_20) -FN:351,(anonymous_21) -FN:360,(anonymous_22) -FN:369,createVaultIndexerWithCache -FNF:24 -FNH:20 -FNDA:1,(anonymous_0) -FNDA:2,(anonymous_1) -FNDA:1,(anonymous_2) -FNDA:0,(anonymous_3) -FNDA:34,(anonymous_4) -FNDA:0,(anonymous_5) -FNDA:18,(anonymous_6) -FNDA:71,(anonymous_7) -FNDA:38,(anonymous_8) -FNDA:53,(anonymous_9) -FNDA:38,(anonymous_10) -FNDA:34,(anonymous_11) -FNDA:0,(anonymous_12) -FNDA:63,(anonymous_13) -FNDA:63,(anonymous_14) -FNDA:112,(anonymous_15) -FNDA:469,(anonymous_16) -FNDA:69,(anonymous_17) -FNDA:12,(anonymous_18) -FNDA:67,(anonymous_19) -FNDA:10,(anonymous_20) -FNDA:145,(anonymous_21) -FNDA:72,(anonymous_22) -FNDA:0,createVaultIndexerWithCache -DA:4,3 -DA:16,1 -DA:20,2 -DA:24,1 -DA:25,1 -DA:29,0 -DA:30,0 -DA:35,34 -DA:38,0 -DA:42,18 -DA:74,71 -DA:78,71 -DA:79,71 -DA:83,38 -DA:84,3 -DA:87,35 -DA:88,0 -DA:91,35 -DA:92,35 -DA:94,3 -DA:95,3 -DA:98,0 -DA:100,3 -DA:101,1 -DA:102,1 -DA:103,1 -DA:110,34 -DA:111,34 -DA:112,34 -DA:113,34 -DA:115,34 -DA:116,53 -DA:117,38 -DA:120,34 -DA:121,2 -DA:122,2 -DA:124,0 -DA:131,34 -DA:139,34 -DA:140,34 -DA:141,34 -DA:142,34 -DA:146,34 -DA:147,0 -DA:148,0 -DA:152,34 -DA:153,34 -DA:154,18 -DA:155,0 -DA:158,18 -DA:159,18 -DA:161,63 -DA:162,63 -DA:163,62 -DA:164,62 -DA:165,62 -DA:166,53 -DA:172,53 -DA:173,53 -DA:174,53 -DA:176,0 -DA:178,9 -DA:180,1 -DA:184,1 -DA:189,18 -DA:190,63 -DA:192,18 -DA:200,34 -DA:203,34 -DA:207,112 -DA:234,112 -DA:237,469 -DA:241,69 -DA:242,69 -DA:243,69 -DA:246,69 -DA:247,69 -DA:248,2 -DA:249,2 -DA:250,2 -DA:251,2 -DA:252,3 -DA:253,3 -DA:254,3 -DA:255,3 -DA:256,1 -DA:257,1 -DA:259,2 -DA:260,2 -DA:261,2 -DA:266,0 -DA:270,69 -DA:271,69 -DA:272,12 -DA:275,69 -DA:276,69 -DA:277,69 -DA:280,69 -DA:286,69 -DA:288,69 -DA:296,67 -DA:297,67 -DA:299,67 -DA:300,73 -DA:301,73 -DA:302,73 -DA:304,73 -DA:308,0 -DA:309,0 -DA:310,73 -DA:315,1 -DA:316,1 -DA:319,73 -DA:320,1 -DA:321,1 -DA:324,73 -DA:325,4 -DA:326,4 -DA:329,73 -DA:330,58 -DA:331,58 -DA:334,73 -DA:335,56 -DA:336,56 -DA:339,73 -DA:340,63 -DA:341,63 -DA:345,67 -DA:353,145 -DA:354,145 -DA:355,144 -DA:356,144 -DA:357,122 -DA:361,72 -DA:362,72 -DA:366,3 -DA:369,3 -DA:370,0 -LF:138 -LH:124 -BRDA:20,0,0,2 -BRDA:20,0,1,1 -BRDA:82,1,0,5 -BRDA:83,2,0,3 -BRDA:83,3,0,38 -BRDA:83,3,1,36 -BRDA:87,4,0,0 -BRDA:92,5,0,3 -BRDA:100,6,0,1 -BRDA:120,7,0,2 -BRDA:125,8,0,0 -BRDA:125,8,1,0 -BRDA:147,9,0,0 -BRDA:154,10,0,0 -BRDA:165,11,0,53 -BRDA:172,12,0,53 -BRDA:181,13,0,1 -BRDA:181,13,1,0 -BRDA:237,14,0,469 -BRDA:237,14,1,442 -BRDA:247,15,0,2 -BRDA:253,16,0,0 -BRDA:255,17,0,1 -BRDA:255,17,1,2 -BRDA:256,18,0,1 -BRDA:259,19,0,2 -BRDA:260,20,0,2 -BRDA:271,21,0,11 -BRDA:276,22,0,69 -BRDA:304,23,0,0 -BRDA:304,23,1,73 -BRDA:305,24,0,73 -BRDA:305,24,1,0 -BRDA:310,25,0,1 -BRDA:311,26,0,73 -BRDA:311,26,1,65 -BRDA:311,26,2,65 -BRDA:319,27,0,1 -BRDA:319,28,0,73 -BRDA:319,28,1,1 -BRDA:324,29,0,4 -BRDA:329,30,0,58 -BRDA:329,31,0,73 -BRDA:329,31,1,63 -BRDA:334,32,0,56 -BRDA:339,33,0,63 -BRDA:353,34,0,0 -BRDA:354,35,0,1 -BRDA:355,36,0,0 -BRDA:355,37,0,144 -BRDA:355,37,1,0 -BRDA:356,38,0,22 -BRDA:356,39,0,144 -BRDA:356,39,1,22 -BRF:54 -BRH:42 -end_of_record diff --git a/src/ollama-client.ts b/src/ollama-client.ts index 7577f06..5016b25 100644 --- a/src/ollama-client.ts +++ b/src/ollama-client.ts @@ -189,9 +189,11 @@ export class OllamaClient { } finally { // Abort the local controller controller.abort(); + // Clean up the reference only if this is still the current stream + if (this.currentStreamController === controller) { + this.currentStreamController = null; + } } - // Clean up the reference - this.currentStreamController = null; } async chat(messages: OllamaMessage[], tools: OllamaTool[] = []): Promise { diff --git a/tests/ollama-client.test.ts b/tests/ollama-client.test.ts index 3ac7c4f..2f3a95d 100755 --- a/tests/ollama-client.test.ts +++ b/tests/ollama-client.test.ts @@ -569,6 +569,102 @@ describe('OllamaClient', () => { }); }); + describe('cancelStream race condition', () => { + it('should not clear new stream controller when old stream finally block executes', async () => { + // Race condition scenario: + // 1. Stream A starts → controllerA assigned to currentStreamController + // 2. Stream B starts → controllerB replaces controllerA + // 3. Stream A's fetch rejects (abort) → finally block runs + // 4. Guard (this.currentStreamController === controllerA) is false + // 5. controllerB survives + + // First fetch call: pending promise with manually triggerable abort + let fireFirstAbort: (() => void) | undefined; + mockFetch.mockImplementationOnce((_url, options?: any) => { + const signal = options?.signal; + return new Promise((_, reject) => { + if (signal?.aborted) { + reject(new DOMException('The operation was aborted.', 'AbortError')); + return; + } + signal?.addEventListener( + 'abort', + () => { + reject(new DOMException('The operation was aborted.', 'AbortError')); + }, + { once: true } + ); + // Capture abort trigger for manual control + fireFirstAbort = () => { + signal?.dispatchEvent(new CustomEvent('abort')); + }; + }); + }); + + // Second fetch call: also pending, so controller stays assigned + // We don't need it to complete - just need to verify controller survives abort + mockFetch.mockImplementationOnce((_url, options?: any) => { + const signal = options?.signal; + return new Promise((_, reject) => { + if (signal?.aborted) { + reject(new DOMException('The operation was aborted.', 'AbortError')); + return; + } + signal?.addEventListener( + 'abort', + () => { + reject(new DOMException('The operation was aborted.', 'AbortError')); + }, + { once: true } + ); + }); + }); + + // Start first stream and consume it (triggers fetch + controller assignment) + const stream1 = await client.streamChat(mockMessages, mockTools); + const consumeFirst = async () => { + for await (const _ of stream1) { + /* consume */ + } + }; + const firstPromise = consumeFirst(); + + // Wait for fetch to be triggered (controller should be set) + await new Promise((resolve) => setTimeout(resolve, 10)); + const firstController = client['currentStreamController']; + expect(firstController).not.toBeNull(); + + // Start second stream and consume it (triggers fetch + replaces controller) + const stream2 = await client.streamChat(mockMessages, mockTools); + const consumeSecond = async () => { + for await (const _ of stream2) { + /* consume */ + } + }; + const secondPromise = consumeSecond(); + + // Wait for second stream fetch to trigger and assign its controller + await new Promise((resolve) => setTimeout(resolve, 10)); + + const secondController = client['currentStreamController']; + expect(secondController).not.toBeNull(); + expect(secondController).not.toBe(firstController); + + // Defer abort to next microtask so Jest associates rejection with expectation + // Then immediately await the rejection + queueMicrotask(() => fireFirstAbort?.()); + await expect(firstPromise).rejects.toThrow('The operation was aborted.'); + + // Second stream's controller should still be intact + // (not cleared by first stream's finally block due to guard) + expect(client['currentStreamController']).toBe(secondController); + + // Clean up second stream so it doesn't leak + secondController?.abort(); + await secondPromise.catch(() => {}); + }); + }); + describe('streamChat final buffer parsing', () => { it('should parse final buffer content when stream ends with partial line', async () => { const streamData = [