Remove deprecated files and update coverage reports

This commit is contained in:
2026-05-06 21:52:58 +02:00
parent ccb0603d0c
commit 333328804a
26 changed files with 2339 additions and 3816 deletions
Executable → Regular
View File
View File
+448 -448
View File
File diff suppressed because it is too large Load Diff
+122 -122
View File
@@ -23,30 +23,30 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">94.23% </span>
<span class="strong">5.76% </span>
<span class="quiet">Statements</span>
<span class='fraction'>49/52</span>
<span class='fraction'>3/52</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">87.5% </span>
<span class="strong">0% </span>
<span class="quiet">Branches</span>
<span class='fraction'>42/48</span>
<span class='fraction'>0/48</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="strong">0% </span>
<span class="quiet">Functions</span>
<span class='fraction'>12/12</span>
<span class='fraction'>0/12</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">94.23% </span>
<span class="strong">5.76% </span>
<span class="quiet">Lines</span>
<span class='fraction'>49/52</span>
<span class='fraction'>3/52</span>
</div>
@@ -61,7 +61,7 @@
</div>
</template>
</div>
<div class='status-line high'></div>
<div class='status-line low'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
@@ -214,48 +214,48 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -264,73 +264,73 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">// src/error-handler.ts
@@ -348,124 +348,124 @@ import {
} from './types';
&nbsp;
export class ErrorHandler {
static handleError(error: unknown, context?: string): void {
const message = this.getUserFriendlyMessage(error);
new Notice(message);
<span class="fstat-no" title="function not covered" > static </span>handleError(error: unknown, context?: string): void {
const message = <span class="cstat-no" title="statement not covered" >this.getUserFriendlyMessage(error);</span>
<span class="cstat-no" title="statement not covered" > new Notice(message);</span>
&nbsp;
if (error instanceof Error) {
const ctx = context ? ` [${context}]` : '';
<span class="cstat-no" title="statement not covered" > if (error instanceof Error) {</span>
const ctx = <span class="cstat-no" title="statement not covered" >context ? ` [${context}]` : '';</span>
// 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);
<span class="cstat-no" title="statement not covered" > console.error(`Ollama Plugin Error${ctx}: ${error.message}`);</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (error.stack) {</span>
<span class="cstat-no" title="statement not covered" > console.error(error.stack);</span>
}
} else {
const ctx = context ? <span class="branch-0 cbranch-no" title="branch not covered" >` [${context}]` </span>: '';
console.error(`Ollama Plugin Error${ctx}:`, error);
const ctx = <span class="cstat-no" title="statement not covered" >context ? ` [${context}]` : '';</span>
<span class="cstat-no" title="statement not covered" > console.error(`Ollama Plugin Error${ctx}:`, error);</span>
}
}
&nbsp;
private static getUserFriendlyMessage(error: unknown): string {
if (error instanceof OllamaError) {
return this.getUserFriendlyMessageFromOllamaError(error);
private <span class="fstat-no" title="function not covered" >static </span>getUserFriendlyMessage(error: unknown): string {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (error instanceof OllamaError) {</span>
<span class="cstat-no" title="statement not covered" > return this.getUserFriendlyMessageFromOllamaError(error);</span>
}
&nbsp;
if (error instanceof Error) {
return this.getUserFriendlyMessageFromError(error);
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (error instanceof Error) {</span>
<span class="cstat-no" title="statement not covered" > return this.getUserFriendlyMessageFromError(error);</span>
}
&nbsp;
return 'An unexpected error occurred';
<span class="cstat-no" title="statement not covered" > return 'An unexpected error occurred';</span>
}
&nbsp;
private static getUserFriendlyMessageFromOllamaError(error: OllamaError): string {
switch (error.type) {
private <span class="fstat-no" title="function not covered" >static </span>getUserFriendlyMessageFromOllamaError(error: OllamaError): string {
<span class="cstat-no" title="statement not covered" > switch (error.type) {</span>
case ErrorType.NETWORK_ERROR:
return 'Connection error. Please check if Ollama is running.';
<span class="branch-1 cbranch-no" title="branch not covered" > case ErrorType.API_ERROR:</span>
<span class="cstat-no" title="statement not covered" > return 'Connection error. Please check if Ollama is running.';</span>
case ErrorType.API_ERROR:
<span class="cstat-no" title="statement not covered" > return `API error: ${error.message}`;</span>
case ErrorType.VALIDATION_ERROR:
return this.getUserFriendlyValidationMessage(error);
<span class="cstat-no" title="statement not covered" > return this.getUserFriendlyValidationMessage(error);</span>
case ErrorType.STREAMING_ERROR:
return 'Response too long. Please try a shorter request.';
<span class="cstat-no" title="statement not covered" > return 'Response too long. Please try a shorter request.';</span>
case ErrorType.TOOL_EXECUTION_ERROR:
return `Tool error for ${(error as ToolExecutionError).toolName}. ${error.message}`;
<span class="cstat-no" title="statement not covered" > return `Tool error for ${(error as ToolExecutionError).toolName}. ${error.message}`;</span>
case ErrorType.PATH_VALIDATION_ERROR:
return `Invalid file path: ${(error as PathValidationError).path}`;
<span class="branch-6 cbranch-no" title="branch not covered" > case ErrorType.UNKNOWN_ERROR:</span>
<span class="cstat-no" title="statement not covered" > return `Invalid file path: ${(error as PathValidationError).path}`;</span>
case ErrorType.UNKNOWN_ERROR:
<span class="cstat-no" title="statement not covered" > return 'An unexpected error occurred';</span>
<span class="branch-7 cbranch-no" title="branch not covered" > default:</span>
default:
<span class="cstat-no" title="statement not covered" > return 'An unexpected error occurred';</span>
}
}
&nbsp;
private static getUserFriendlyValidationMessage(error: OllamaError): string {
if (error instanceof ValidationError &amp;&amp; error.details?.field) {
const fieldMsg = error.details.field.charAt(0).toUpperCase() + error.details.field.slice(1);
return `Invalid ${fieldMsg.toLowerCase()}. ${error.details.message ?? <span class="branch-1 cbranch-no" title="branch not covered" >error.message}</span>`;
private <span class="fstat-no" title="function not covered" >static </span>getUserFriendlyValidationMessage(error: OllamaError): string {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (error instanceof ValidationError &amp;&amp; error.details?.field) {</span>
const fieldMsg = <span class="cstat-no" title="statement not covered" >error.details.field.charAt(0).toUpperCase() + error.details.field.slice(1);</span>
<span class="cstat-no" title="statement not covered" > return `Invalid ${fieldMsg.toLowerCase()}. ${error.details.message ?? error.message}`;</span>
}
return 'Input validation error. Please correct your input.';
<span class="cstat-no" title="statement not covered" > return 'Input validation error. Please correct your input.';</span>
}
&nbsp;
private static getUserFriendlyMessageFromError(error: Error): string {
const msg = error.message.toLowerCase();
private <span class="fstat-no" title="function not covered" >static </span>getUserFriendlyMessageFromError(error: Error): string {
const msg = <span class="cstat-no" title="statement not covered" >error.message.toLowerCase();</span>
&nbsp;
// 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.';
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (msg.includes('timeout') || msg.includes('timed out') || msg.includes('time out')) {</span>
<span class="cstat-no" title="statement not covered" > return 'Request timed out. Please check your Ollama connection.';</span>
}
&nbsp;
if (msg.includes('network') || msg.includes('connection') || msg.includes('fetch')) {
return 'Connection error. Please check if Ollama is running.';
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (msg.includes('network') || msg.includes('connection') || msg.includes('fetch')) {</span>
<span class="cstat-no" title="statement not covered" > return 'Connection error. Please check if Ollama is running.';</span>
}
&nbsp;
if (msg.includes('validation') || msg.includes('invalid')) {
return 'Invalid input. Please correct your input.';
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (msg.includes('validation') || msg.includes('invalid')) {</span>
<span class="cstat-no" title="statement not covered" > return 'Invalid input. Please correct your input.';</span>
}
&nbsp;
if (msg.includes('stream') || msg.includes('chunk')) {
return 'Response too long. Please try a shorter request.';
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (msg.includes('stream') || msg.includes('chunk')) {</span>
<span class="cstat-no" title="statement not covered" > return 'Response too long. Please try a shorter request.';</span>
}
&nbsp;
if (msg.includes('tool') || msg.includes('function')) {
return 'Tool error. Please try again.';
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (msg.includes('tool') || msg.includes('function')) {</span>
<span class="cstat-no" title="statement not covered" > return 'Tool error. Please try again.';</span>
}
&nbsp;
if (msg.includes('path') || msg.includes('file')) {
return 'Invalid file path. Please check the path and try again.';
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (msg.includes('path') || msg.includes('file')) {</span>
<span class="cstat-no" title="statement not covered" > return 'Invalid file path. Please check the path and try again.';</span>
}
&nbsp;
return 'An unexpected error occurred';
<span class="cstat-no" title="statement not covered" > return 'An unexpected error occurred';</span>
}
&nbsp;
// -- Factory methods --
&nbsp;
static createNetworkError(message: string, statusCode?: number): NetworkError {
return new NetworkError(message, statusCode);
<span class="fstat-no" title="function not covered" > static </span>createNetworkError(message: string, statusCode?: number): NetworkError {
<span class="cstat-no" title="statement not covered" > return new NetworkError(message, statusCode);</span>
}
&nbsp;
static createApiError(message: string, statusCode?: number): ApiError {
return new ApiError(message, statusCode);
<span class="fstat-no" title="function not covered" > static </span>createApiError(message: string, statusCode?: number): ApiError {
<span class="cstat-no" title="statement not covered" > return new ApiError(message, statusCode);</span>
}
&nbsp;
static createValidationError(message: string, field?: string): ValidationError {
const details = field ? { field, message } : <span class="branch-1 cbranch-no" title="branch not covered" >undefined;</span>
return new ValidationError(message, details);
<span class="fstat-no" title="function not covered" > static </span>createValidationError(message: string, field?: string): ValidationError {
const details = <span class="cstat-no" title="statement not covered" >field ? { field, message } : undefined;</span>
<span class="cstat-no" title="statement not covered" > return new ValidationError(message, details);</span>
}
&nbsp;
static createStreamingError(message: string): StreamingError {
return new StreamingError(message);
<span class="fstat-no" title="function not covered" > static </span>createStreamingError(message: string): StreamingError {
<span class="cstat-no" title="statement not covered" > return new StreamingError(message);</span>
}
&nbsp;
static createToolExecutionError(message: string, toolName?: string): ToolExecutionError {
return new ToolExecutionError(message, toolName ?? 'unknown');
<span class="fstat-no" title="function not covered" > static </span>createToolExecutionError(message: string, toolName?: string): ToolExecutionError {
<span class="cstat-no" title="statement not covered" > return new ToolExecutionError(message, toolName ?? 'unknown');</span>
}
&nbsp;
static createPathValidationError(message: string, path?: string): PathValidationError {
return new PathValidationError(message, path ?? '');
<span class="fstat-no" title="function not covered" > static </span>createPathValidationError(message: string, path?: string): PathValidationError {
<span class="cstat-no" title="statement not covered" > return new PathValidationError(message, path ?? '');</span>
}
&nbsp;
static createUnknownError(message: string): OllamaError {
return new OllamaError(message, ErrorType.UNKNOWN_ERROR);
<span class="fstat-no" title="function not covered" > static </span>createUnknownError(message: string): OllamaError {
<span class="cstat-no" title="statement not covered" > return new OllamaError(message, ErrorType.UNKNOWN_ERROR);</span>
}
}
&nbsp;</pre></td></tr></table></pre>
@@ -475,7 +475,7 @@ export class ErrorHandler {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T19:45:32.568Z
at 2026-05-06T19:51:33.347Z
</div>
<script src="prettify.js"></script>
<script>
Executable → Regular
View File

Before

Width:  |  Height:  |  Size: 445 B

After

Width:  |  Height:  |  Size: 445 B

Executable → Regular
+83 -83
View File
@@ -23,30 +23,30 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">88.83% </span>
<span class="strong">36.65% </span>
<span class="quiet">Statements</span>
<span class='fraction'>589/663</span>
<span class='fraction'>243/663</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">77.02% </span>
<span class="strong">19.25% </span>
<span class="quiet">Branches</span>
<span class='fraction'>228/296</span>
<span class='fraction'>57/296</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">86% </span>
<span class="strong">28% </span>
<span class="quiet">Functions</span>
<span class='fraction'>86/100</span>
<span class='fraction'>28/100</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">90.07% </span>
<span class="strong">37.16% </span>
<span class="quiet">Lines</span>
<span class='fraction'>572/635</span>
<span class='fraction'>236/635</span>
</div>
@@ -61,7 +61,7 @@
</div>
</template>
</div>
<div class='status-line high'></div>
<div class='status-line low'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
@@ -79,108 +79,108 @@
</tr>
</thead>
<tbody><tr>
<td class="file high" data-value="chat-view.ts"><a href="chat-view.ts.html">chat-view.ts</a></td>
<td data-value="91.2" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 91%"></div><div class="cover-empty" style="width: 9%"></div></div>
<td class="file medium" data-value="chat-view.ts"><a href="chat-view.ts.html">chat-view.ts</a></td>
<td data-value="58.79" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 58%"></div><div class="cover-empty" style="width: 42%"></div></div>
</td>
<td data-value="91.2" class="pct high">91.2%</td>
<td data-value="216" class="abs high">197/216</td>
<td data-value="82.85" class="pct high">82.85%</td>
<td data-value="70" class="abs high">58/70</td>
<td data-value="82.35" class="pct high">82.35%</td>
<td data-value="34" class="abs high">28/34</td>
<td data-value="93.1" class="pct high">93.1%</td>
<td data-value="203" class="abs high">189/203</td>
<td data-value="58.79" class="pct medium">58.79%</td>
<td data-value="216" class="abs medium">127/216</td>
<td data-value="41.42" class="pct low">41.42%</td>
<td data-value="70" class="abs low">29/70</td>
<td data-value="38.23" class="pct low">38.23%</td>
<td data-value="34" class="abs low">13/34</td>
<td data-value="60.09" class="pct medium">60.09%</td>
<td data-value="203" class="abs medium">122/203</td>
</tr>
<tr>
<td class="file high" data-value="error-handler.ts"><a href="error-handler.ts.html">error-handler.ts</a></td>
<td data-value="94.23" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 94%"></div><div class="cover-empty" style="width: 6%"></div></div>
<td class="file low" data-value="error-handler.ts"><a href="error-handler.ts.html">error-handler.ts</a></td>
<td data-value="5.76" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 5%"></div><div class="cover-empty" style="width: 95%"></div></div>
</td>
<td data-value="94.23" class="pct high">94.23%</td>
<td data-value="52" class="abs high">49/52</td>
<td data-value="87.5" class="pct high">87.5%</td>
<td data-value="48" class="abs high">42/48</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="12" class="abs high">12/12</td>
<td data-value="94.23" class="pct high">94.23%</td>
<td data-value="52" class="abs high">49/52</td>
<td data-value="5.76" class="pct low">5.76%</td>
<td data-value="52" class="abs low">3/52</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="48" class="abs low">0/48</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="12" class="abs low">0/12</td>
<td data-value="5.76" class="pct low">5.76%</td>
<td data-value="52" class="abs low">3/52</td>
</tr>
<tr>
<td class="file medium" data-value="ollama-client.ts"><a href="ollama-client.ts.html">ollama-client.ts</a></td>
<td data-value="77.65" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 77%"></div><div class="cover-empty" style="width: 23%"></div></div>
<td class="file low" data-value="ollama-client.ts"><a href="ollama-client.ts.html">ollama-client.ts</a></td>
<td data-value="8.51" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 8%"></div><div class="cover-empty" style="width: 92%"></div></div>
</td>
<td data-value="77.65" class="pct medium">77.65%</td>
<td data-value="94" class="abs medium">73/94</td>
<td data-value="61.22" class="pct medium">61.22%</td>
<td data-value="49" class="abs medium">30/49</td>
<td data-value="90.9" class="pct high">90.9%</td>
<td data-value="11" class="abs high">10/11</td>
<td data-value="78.88" class="pct medium">78.88%</td>
<td data-value="90" class="abs medium">71/90</td>
<td data-value="8.51" class="pct low">8.51%</td>
<td data-value="94" class="abs low">8/94</td>
<td data-value="4.08" class="pct low">4.08%</td>
<td data-value="49" class="abs low">2/49</td>
<td data-value="9.09" class="pct low">9.09%</td>
<td data-value="11" class="abs low">1/11</td>
<td data-value="8.88" class="pct low">8.88%</td>
<td data-value="90" class="abs low">8/90</td>
</tr>
<tr>
<td class="file high" data-value="tool-executor.ts"><a href="tool-executor.ts.html">tool-executor.ts</a></td>
<td data-value="91.37" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 91%"></div><div class="cover-empty" style="width: 9%"></div></div>
<td class="file medium" data-value="tool-executor.ts"><a href="tool-executor.ts.html">tool-executor.ts</a></td>
<td data-value="62.06" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 62%"></div><div class="cover-empty" style="width: 38%"></div></div>
</td>
<td data-value="91.37" class="pct high">91.37%</td>
<td data-value="58" class="abs high">53/58</td>
<td data-value="78.78" class="pct medium">78.78%</td>
<td data-value="33" class="abs medium">26/33</td>
<td data-value="62.06" class="pct medium">62.06%</td>
<td data-value="58" class="abs medium">36/58</td>
<td data-value="30.3" class="pct low">30.3%</td>
<td data-value="33" class="abs low">10/33</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="4" class="abs high">4/4</td>
<td data-value="91.37" class="pct high">91.37%</td>
<td data-value="58" class="abs high">53/58</td>
<td data-value="62.06" class="pct medium">62.06%</td>
<td data-value="58" class="abs medium">36/58</td>
</tr>
<tr>
<td class="file high" data-value="types.ts"><a href="types.ts.html">types.ts</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
<td class="file low" data-value="types.ts"><a href="types.ts.html">types.ts</a></td>
<td data-value="44.44" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 44%"></div><div class="cover-empty" style="width: 56%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="36" class="abs high">36/36</td>
<td data-value="44.44" class="pct low">44.44%</td>
<td data-value="36" class="abs low">16/36</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="2" class="abs high">2/2</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="8" class="abs high">8/8</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="36" class="abs high">36/36</td>
<td data-value="12.5" class="pct low">12.5%</td>
<td data-value="8" class="abs low">1/8</td>
<td data-value="44.44" class="pct low">44.44%</td>
<td data-value="36" class="abs low">16/36</td>
</tr>
<tr>
<td class="file high" data-value="utils.ts"><a href="utils.ts.html">utils.ts</a></td>
<td data-value="81.72" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 81%"></div><div class="cover-empty" style="width: 19%"></div></div>
<td class="file low" data-value="utils.ts"><a href="utils.ts.html">utils.ts</a></td>
<td data-value="35.48" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 35%"></div><div class="cover-empty" style="width: 65%"></div></div>
</td>
<td data-value="81.72" class="pct high">81.72%</td>
<td data-value="93" class="abs high">76/93</td>
<td data-value="70.83" class="pct medium">70.83%</td>
<td data-value="48" class="abs medium">34/48</td>
<td data-value="56.25" class="pct medium">56.25%</td>
<td data-value="16" class="abs medium">9/16</td>
<td data-value="82.22" class="pct high">82.22%</td>
<td data-value="90" class="abs high">74/90</td>
<td data-value="35.48" class="pct low">35.48%</td>
<td data-value="93" class="abs low">33/93</td>
<td data-value="20.83" class="pct low">20.83%</td>
<td data-value="48" class="abs low">10/48</td>
<td data-value="25" class="pct low">25%</td>
<td data-value="16" class="abs low">4/16</td>
<td data-value="35.55" class="pct low">35.55%</td>
<td data-value="90" class="abs low">32/90</td>
</tr>
<tr>
<td class="file high" data-value="vault-indexer.ts"><a href="vault-indexer.ts.html">vault-indexer.ts</a></td>
<td data-value="92.1" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 92%"></div><div class="cover-empty" style="width: 8%"></div></div>
<td class="file low" data-value="vault-indexer.ts"><a href="vault-indexer.ts.html">vault-indexer.ts</a></td>
<td data-value="17.54" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 17%"></div><div class="cover-empty" style="width: 83%"></div></div>
</td>
<td data-value="92.1" class="pct high">92.1%</td>
<td data-value="114" class="abs high">105/114</td>
<td data-value="78.26" class="pct medium">78.26%</td>
<td data-value="46" class="abs medium">36/46</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="15" class="abs high">15/15</td>
<td data-value="94.33" class="pct high">94.33%</td>
<td data-value="106" class="abs high">100/106</td>
<td data-value="17.54" class="pct low">17.54%</td>
<td data-value="114" class="abs low">20/114</td>
<td data-value="8.69" class="pct low">8.69%</td>
<td data-value="46" class="abs low">4/46</td>
<td data-value="33.33" class="pct low">33.33%</td>
<td data-value="15" class="abs low">5/15</td>
<td data-value="17.92" class="pct low">17.92%</td>
<td data-value="106" class="abs low">19/106</td>
</tr>
</tbody>
@@ -191,7 +191,7 @@
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T19:45:32.568Z
at 2026-05-06T19:51:33.347Z
</div>
<script src="prettify.js"></script>
<script>
+279 -279
View File
@@ -23,30 +23,30 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">77.65% </span>
<span class="strong">8.51% </span>
<span class="quiet">Statements</span>
<span class='fraction'>73/94</span>
<span class='fraction'>8/94</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">61.22% </span>
<span class="strong">4.08% </span>
<span class="quiet">Branches</span>
<span class='fraction'>30/49</span>
<span class='fraction'>2/49</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">90.9% </span>
<span class="strong">9.09% </span>
<span class="quiet">Functions</span>
<span class='fraction'>10/11</span>
<span class='fraction'>1/11</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">78.88% </span>
<span class="strong">8.88% </span>
<span class="quiet">Lines</span>
<span class='fraction'>71/90</span>
<span class='fraction'>8/90</span>
</div>
@@ -61,7 +61,7 @@
</div>
</template>
</div>
<div class='status-line medium'></div>
<div class='status-line low'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
@@ -305,216 +305,216 @@
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">48x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">48x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">48x</span>
<span class="cline-any cline-yes">48x</span>
<span class="cline-any cline-yes">48x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">61x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">61x</span>
<span class="cline-any cline-yes">61x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">53x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">52x</span>
<span class="cline-any cline-yes">52x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">51x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -544,40 +544,40 @@ export class OllamaClient {
this.fetchFn = fetchFn ?? fetch;
}
&nbsp;
cancelStream(): void {
<span class="missing-if-branch" title="if path not taken" >I</span>if (this.abortController) {
<span class="fstat-no" title="function not covered" > cancelStream(</span>): void {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (this.abortController) {</span>
<span class="cstat-no" title="statement not covered" > this.abortController.abort();</span>
<span class="cstat-no" title="statement not covered" > this.abortController = null;</span>
}
}
&nbsp;
async *streamChat(
<span class="fstat-no" title="function not covered" > async </span>*streamChat(
messages: OllamaMessage[],
tools: OllamaTool[] = <span class="branch-0 cbranch-no" title="branch not covered" >[]</span>
): AsyncGenerator&lt;OllamaMessage, void, unknown&gt; {
yield* this.streamChatWithRetry(messages, tools, 0);
<span class="cstat-no" title="statement not covered" > yield* this.streamChatWithRetry(messages, tools, 0);</span>
}
&nbsp;
async streamChatAsPromise(
<span class="fstat-no" title="function not covered" > async </span>streamChatAsPromise(
messages: OllamaMessage[],
tools: OllamaTool[] = <span class="branch-0 cbranch-no" title="branch not covered" >[]</span>
): Promise&lt;OllamaMessage[]&gt; {
const chunks: OllamaMessage[] = [];
for await (const chunk of this.streamChat(messages, tools)) {
const chunks: OllamaMessage[] = <span class="cstat-no" title="statement not covered" >[];</span>
<span class="cstat-no" title="statement not covered" > for await (const chunk of this.streamChat(messages, tools)) {</span>
<span class="cstat-no" title="statement not covered" > chunks.push(chunk);</span>
}
<span class="cstat-no" title="statement not covered" > return chunks;</span>
}
&nbsp;
private async *streamChatWithRetry(
private <span class="fstat-no" title="function not covered" >async </span>*streamChatWithRetry(
messages: OllamaMessage[],
tools: OllamaTool[] = <span class="branch-0 cbranch-no" title="branch not covered" >[],</span>
attempt: number = <span class="branch-0 cbranch-no" title="branch not covered" >0</span>
): AsyncGenerator&lt;OllamaMessage, void, unknown&gt; {
this.abortController = new AbortController();
<span class="cstat-no" title="statement not covered" > this.abortController = new AbortController();</span>
&nbsp;
try {
const response = await this.fetchFn(`${this.baseURL}/api/chat`, {
<span class="cstat-no" title="statement not covered" > try {</span>
const response = <span class="cstat-no" title="statement not covered" >await this.fetchFn(`${this.baseURL}/api/chat`, {</span>
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -591,8 +591,8 @@ export class OllamaClient {
signal: this.abortController.signal,
});
&nbsp;
if (!response.ok) {
<span class="missing-if-branch" title="if path not taken" >I</span>if (response.status &gt;= 500 &amp;&amp; <span class="branch-1 cbranch-no" title="branch not covered" >attempt &lt; this.maxRetries)</span> {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!response.ok) {</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (response.status &gt;= 500 &amp;&amp; attempt &lt; this.maxRetries) {</span>
const retryDelay = <span class="cstat-no" title="statement not covered" >Math.pow(2, attempt) * 100;</span>
<span class="cstat-no" title="statement not covered" > Logger.warn(</span>
`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
@@ -602,59 +602,59 @@ export class OllamaClient {
<span class="cstat-no" title="statement not covered" > yield* this.streamChatWithRetry(messages, tools, attempt + 1);</span>
<span class="cstat-no" title="statement not covered" > return;</span>
}
throw new ApiError(`Ollama API error: ${response.status}`, response.status);
<span class="cstat-no" title="statement not covered" > throw new ApiError(`Ollama API error: ${response.status}`, response.status);</span>
}
&nbsp;
if (!response.body) {
throw new Error('No response body');
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!response.body) {</span>
<span class="cstat-no" title="statement not covered" > throw new Error('No response body');</span>
}
&nbsp;
const contentType = response.headers.get('content-type');
if (!contentType || (!contentType.includes('ndjson') &amp;&amp; !contentType.includes('json'))) {
throw new Error('Invalid response format');
const contentType = <span class="cstat-no" title="statement not covered" >response.headers.get('content-type');</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!contentType || (!contentType.includes('ndjson') &amp;&amp; !contentType.includes('json'))) {</span>
<span class="cstat-no" title="statement not covered" > throw new Error('Invalid response format');</span>
}
&nbsp;
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let malformedCount = 0;
const maxMalformed = 50;
const reader = <span class="cstat-no" title="statement not covered" >response.body.getReader();</span>
const decoder = <span class="cstat-no" title="statement not covered" >new TextDecoder();</span>
let buffer = <span class="cstat-no" title="statement not covered" >'';</span>
let malformedCount = <span class="cstat-no" title="statement not covered" >0;</span>
const maxMalformed = <span class="cstat-no" title="statement not covered" >50;</span>
&nbsp;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
<span class="cstat-no" title="statement not covered" > try {</span>
<span class="cstat-no" title="statement not covered" > while (true) {</span>
const { done, value } = <span class="cstat-no" title="statement not covered" >await reader.read();</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (done) <span class="cstat-no" title="statement not covered" >break;</span></span>
&nbsp;
buffer += decoder.decode(value, { stream: true });
<span class="cstat-no" title="statement not covered" > buffer += decoder.decode(value, { stream: true });</span>
&nbsp;
const lines = buffer.split('\n');
buffer = lines.pop() ?? <span class="branch-1 cbranch-no" title="branch not covered" >'';</span>
const lines = <span class="cstat-no" title="statement not covered" >buffer.split('\n');</span>
<span class="cstat-no" title="statement not covered" > buffer = lines.pop() ?? '';</span>
&nbsp;
for (const line of lines) {
<span class="missing-if-branch" title="if path not taken" >I</span>if (!line.trim()) <span class="cstat-no" title="statement not covered" >continue;</span>
<span class="cstat-no" title="statement not covered" > for (const line of lines) {</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!line.trim()) <span class="cstat-no" title="statement not covered" >continue;</span></span>
&nbsp;
try {
const parsed = JSON.parse(line) as Record&lt;string, unknown&gt;;
this.throwIfOllamaError(parsed);
<span class="cstat-no" title="statement not covered" > try {</span>
const parsed = <span class="cstat-no" title="statement not covered" >JSON.parse(line) as Record&lt;string, unknown&gt;;</span>
<span class="cstat-no" title="statement not covered" > this.throwIfOllamaError(parsed);</span>
&nbsp;
const message = this.toOllamaMessage(parsed.message);
<span class="missing-if-branch" title="if path not taken" >I</span>if (!message) {
const message = <span class="cstat-no" title="statement not covered" >this.toOllamaMessage(parsed.message);</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!message) {</span>
<span class="cstat-no" title="statement not covered" > continue;</span>
}
&nbsp;
malformedCount = 0;
yield message;
<span class="cstat-no" title="statement not covered" > malformedCount = 0;</span>
<span class="cstat-no" title="statement not covered" > yield message;</span>
} catch (error) {
if (error instanceof Error &amp;&amp; error.message.startsWith('Ollama error:')) {
throw error;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (error instanceof Error &amp;&amp; error.message.startsWith('Ollama error:')) {</span>
<span class="cstat-no" title="statement not covered" > throw error;</span>
}
&nbsp;
malformedCount++;
if (malformedCount &gt; maxMalformed) {
throw new Error('Too many malformed chunks in stream');
<span class="cstat-no" title="statement not covered" > malformedCount++;</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (malformedCount &gt; maxMalformed) {</span>
<span class="cstat-no" title="statement not covered" > throw new Error('Too many malformed chunks in stream');</span>
}
&nbsp;
Logger.warn(
<span class="cstat-no" title="statement not covered" > Logger.warn(</span>
`Skipped malformed chunk: ${line.substring(0, 80)}... - ${(error as Error).message}`,
'ollama-client'
);
@@ -662,7 +662,7 @@ export class OllamaClient {
}
}
&nbsp;
<span class="missing-if-branch" title="if path not taken" >I</span>if (buffer.trim()) {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (buffer.trim()) {</span>
<span class="cstat-no" title="statement not covered" > try {</span>
const parsed = <span class="cstat-no" title="statement not covered" >JSON.parse(buffer) as Record&lt;string, unknown&gt;;</span>
<span class="cstat-no" title="statement not covered" > this.throwIfOllamaError(parsed);</span>
@@ -682,25 +682,25 @@ export class OllamaClient {
}
}
} finally {
reader.releaseLock();
<span class="cstat-no" title="statement not covered" > reader.releaseLock();</span>
}
} finally {
this.abortController = null;
<span class="cstat-no" title="statement not covered" > this.abortController = null;</span>
}
}
&nbsp;
async chat(messages: OllamaMessage[], tools: OllamaTool[] = <span class="branch-0 cbranch-no" title="branch not covered" >[])</span>: Promise&lt;OllamaMessage&gt; {
return this.chatWithRetry(messages, tools, 0);
<span class="fstat-no" title="function not covered" > async </span>chat(messages: OllamaMessage[], tools: OllamaTool[] = <span class="branch-0 cbranch-no" title="branch not covered" >[])</span>: Promise&lt;OllamaMessage&gt; {
<span class="cstat-no" title="statement not covered" > return this.chatWithRetry(messages, tools, 0);</span>
}
&nbsp;
private async chatWithRetry(
private <span class="fstat-no" title="function not covered" >async </span>chatWithRetry(
messages: OllamaMessage[],
tools: OllamaTool[] = <span class="branch-0 cbranch-no" title="branch not covered" >[],</span>
attempt: number = <span class="branch-0 cbranch-no" title="branch not covered" >0</span>
): Promise&lt;OllamaMessage&gt; {
const controller = new AbortController();
const controller = <span class="cstat-no" title="statement not covered" >new AbortController();</span>
&nbsp;
const response = await this.fetchFn(`${this.baseURL}/api/chat`, {
const response = <span class="cstat-no" title="statement not covered" >await this.fetchFn(`${this.baseURL}/api/chat`, {</span>
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -714,38 +714,38 @@ export class OllamaClient {
signal: controller.signal,
});
&nbsp;
if (!response.ok) {
if (response.status &gt;= 500 &amp;&amp; attempt &lt; this.maxRetries) {
const retryDelay = Math.pow(2, attempt) * 100;
Logger.warn(
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!response.ok) {</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (response.status &gt;= 500 &amp;&amp; attempt &lt; this.maxRetries) {</span>
const retryDelay = <span class="cstat-no" title="statement not covered" >Math.pow(2, attempt) * 100;</span>
<span class="cstat-no" title="statement not covered" > Logger.warn(</span>
`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
'ollama-client'
);
await new Promise((resolve) =&gt; setTimeout(resolve, retryDelay));
return this.chatWithRetry(messages, tools, attempt + 1);
<span class="cstat-no" title="statement not covered" > await new Promise(<span class="fstat-no" title="function not covered" >(r</span>esolve) =&gt; <span class="cstat-no" title="statement not covered" >setTimeout(resolve, retryDelay))</span>;</span>
<span class="cstat-no" title="statement not covered" > return this.chatWithRetry(messages, tools, attempt + 1);</span>
}
throw new ApiError(`Ollama API error: ${response.status}`, response.status);
<span class="cstat-no" title="statement not covered" > throw new ApiError(`Ollama API error: ${response.status}`, response.status);</span>
}
&nbsp;
const data = (await response.json()) as OllamaChatResponse;
return this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] };
const data = (<span class="cstat-no" title="statement not covered" >await response.json())</span> as OllamaChatResponse;
<span class="cstat-no" title="statement not covered" > return this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] };</span>
}
&nbsp;
private throwIfOllamaError(parsed: Record&lt;string, unknown&gt;): void {
if (parsed.error) {
throw new Error(`Ollama error: ${String(parsed.error)}`);
private <span class="fstat-no" title="function not covered" >throwIfOllamaError(</span>parsed: Record&lt;string, unknown&gt;): void {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (parsed.error) {</span>
<span class="cstat-no" title="statement not covered" > throw new Error(`Ollama error: ${String(parsed.error)}`);</span>
}
}
&nbsp;
private toOllamaMessage(value: unknown): OllamaMessage | null {
if (!value || typeof value !== 'object') {
return null;
private <span class="fstat-no" title="function not covered" >toOllamaMessage(</span>value: unknown): OllamaMessage | null {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!value || typeof value !== 'object') {</span>
<span class="cstat-no" title="statement not covered" > return null;</span>
}
&nbsp;
const record = value as Partial&lt;OllamaMessage&gt;;
return {
const record = <span class="cstat-no" title="statement not covered" >value as Partial&lt;OllamaMessage&gt;;</span>
<span class="cstat-no" title="statement not covered" > return {</span>
role: record.role ?? 'assistant',
content: typeof record.content === 'string' ? record.content : <span class="branch-1 cbranch-no" title="branch not covered" >'',</span>
content: typeof record.content === 'string' ? record.content : '',
tool_calls: record.tool_calls ?? [],
};
}
@@ -757,7 +757,7 @@ export class OllamaClient {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T19:45:32.568Z
at 2026-05-06T19:51:33.347Z
</div>
<script src="prettify.js"></script>
<script>
View File
Executable → Regular
View File
View File

Before

Width:  |  Height:  |  Size: 138 B

After

Width:  |  Height:  |  Size: 138 B

Executable → Regular
View File
+108 -108
View File
@@ -23,16 +23,16 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">91.37% </span>
<span class="strong">62.06% </span>
<span class="quiet">Statements</span>
<span class='fraction'>53/58</span>
<span class='fraction'>36/58</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">78.78% </span>
<span class="strong">30.3% </span>
<span class="quiet">Branches</span>
<span class='fraction'>26/33</span>
<span class='fraction'>10/33</span>
</div>
@@ -44,9 +44,9 @@
<div class='fl pad1y space-right2'>
<span class="strong">91.37% </span>
<span class="strong">62.06% </span>
<span class="quiet">Lines</span>
<span class='fraction'>53/58</span>
<span class='fraction'>36/58</span>
</div>
@@ -61,7 +61,7 @@
</div>
</template>
</div>
<div class='status-line high'></div>
<div class='status-line medium'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
@@ -210,118 +210,118 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">58x</span>
<span class="cline-any cline-yes">58x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">23x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">24x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">23x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -347,23 +347,23 @@ export class ToolExecutor {
&nbsp;
private isSafePath(path: string): boolean {
// Reject empty paths
if (!path || path.trim().length === 0) {
return false;
<span class="missing-if-branch" title="if path not taken" >I</span>if (!path || path.trim().length === 0) {
<span class="cstat-no" title="statement not covered" > return false;</span>
}
&nbsp;
// Reject paths that are too long
if (path.length &gt; MAX_PATH_LENGTH) {
return false;
<span class="missing-if-branch" title="if path not taken" >I</span>if (path.length &gt; MAX_PATH_LENGTH) {
<span class="cstat-no" title="statement not covered" > return false;</span>
}
&nbsp;
// Reject paths with invalid characters
if (INVALID_PATH_CHARS.test(path)) {
return false;
<span class="missing-if-branch" title="if path not taken" >I</span>if (INVALID_PATH_CHARS.test(path)) {
<span class="cstat-no" title="statement not covered" > return false;</span>
}
&nbsp;
// Reject absolute paths
if (path.startsWith('/') || path.startsWith('\\')) {
return false;
<span class="missing-if-branch" title="if path not taken" >I</span>if (path.startsWith('/') || path.startsWith('\\')) {
<span class="cstat-no" title="statement not covered" > return false;</span>
}
&nbsp;
// Reject Windows drive letters (e.g., C:)
@@ -372,14 +372,14 @@ export class ToolExecutor {
}
&nbsp;
// Reject paths containing backslashes (Windows-style path separators)
if (path.includes('\\')) {
return false;
<span class="missing-if-branch" title="if path not taken" >I</span>if (path.includes('\\')) {
<span class="cstat-no" title="statement not covered" > return false;</span>
}
&nbsp;
// Reject paths that traverse to parent directories
const normalized = path.replace(/^(\.\/)+/, '');
if (normalized.split('/').includes('..')) {
return false;
<span class="missing-if-branch" title="if path not taken" >I</span>if (normalized.split('/').includes('..')) {
<span class="cstat-no" title="statement not covered" > return false;</span>
}
&nbsp;
// Reject forbidden directories
@@ -410,11 +410,11 @@ export class ToolExecutor {
try {
parsedArgs = safeParseJson(rawArgs) as Record&lt;string, unknown&gt;;
} catch {
throw new Error('Invalid JSON arguments');
<span class="cstat-no" title="statement not covered" > throw new Error('Invalid JSON arguments');</span>
}
} else if (rawArgs &amp;&amp; typeof rawArgs === 'object') {
parsedArgs = rawArgs;
} else <span class="missing-if-branch" title="else path not taken" >E</span>{
} else <span class="cstat-no" title="statement not covered" ><span class="missing-if-branch" title="else path not taken" >E</span>if (rawArgs &amp;&amp; typeof rawArgs === 'object') {</span>
<span class="cstat-no" title="statement not covered" > parsedArgs = rawArgs;</span>
} else {
<span class="cstat-no" title="statement not covered" > throw new Error('Arguments must be an object or JSON string');</span>
}
&nbsp;
@@ -422,12 +422,12 @@ export class ToolExecutor {
switch (toolName) {
case 'create_file':
return await this.handleCreateFile(parsedArgs);
default:
return { success: false, message: `Unknown tool: ${toolName}` };
<span class="branch-1 cbranch-no" title="branch not covered" > default:</span>
<span class="cstat-no" title="statement not covered" > return { success: false, message: `Unknown tool: ${toolName}` };</span>
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : <span class="branch-1 cbranch-no" title="branch not covered" >String(error);</span>
throw new Error(errorMessage);
const errorMessage = <span class="cstat-no" title="statement not covered" >error instanceof Error ? error.message : String(error);</span>
<span class="cstat-no" title="statement not covered" > throw new Error(errorMessage);</span>
}
}
&nbsp;
@@ -435,24 +435,24 @@ export class ToolExecutor {
const path = args.path;
const content = args.content;
&nbsp;
if (typeof path !== 'string') {
throw new Error('Path must be a string');
<span class="missing-if-branch" title="if path not taken" >I</span>if (typeof path !== 'string') {
<span class="cstat-no" title="statement not covered" > throw new Error('Path must be a string');</span>
}
&nbsp;
if (typeof content !== 'string') {
throw new Error('Content must be a string');
<span class="missing-if-branch" title="if path not taken" >I</span>if (typeof content !== 'string') {
<span class="cstat-no" title="statement not covered" > throw new Error('Content must be a string');</span>
}
&nbsp;
if (!this.isSafePath(path)) {
throw new Error('Invalid file path detected');
<span class="missing-if-branch" title="if path not taken" >I</span>if (!this.isSafePath(path)) {
<span class="cstat-no" title="statement not covered" > throw new Error('Invalid file path detected');</span>
}
&nbsp;
try {
await this.vault.create(path, content);
return { success: true, message: 'File created successfully' };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : <span class="branch-1 cbranch-no" title="branch not covered" >String(error);</span>
throw new Error(errorMessage);
const errorMessage = <span class="cstat-no" title="statement not covered" >error instanceof Error ? error.message : String(error);</span>
<span class="cstat-no" title="statement not covered" > throw new Error(errorMessage);</span>
}
}
}
@@ -463,7 +463,7 @@ export class ToolExecutor {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T19:45:32.568Z
at 2026-05-06T19:51:33.347Z
</div>
<script src="prettify.js"></script>
<script>
+55 -55
View File
@@ -23,9 +23,9 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="strong">44.44% </span>
<span class="quiet">Statements</span>
<span class='fraction'>36/36</span>
<span class='fraction'>16/36</span>
</div>
@@ -37,16 +37,16 @@
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="strong">12.5% </span>
<span class="quiet">Functions</span>
<span class='fraction'>8/8</span>
<span class='fraction'>1/8</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="strong">44.44% </span>
<span class="quiet">Lines</span>
<span class='fraction'>36/36</span>
<span class='fraction'>16/36</span>
</div>
@@ -61,7 +61,7 @@
</div>
</template>
</div>
<div class='status-line high'></div>
<div class='status-line low'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
@@ -269,9 +269,9 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -279,9 +279,9 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -289,9 +289,9 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -304,16 +304,16 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -321,9 +321,9 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -331,9 +331,9 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -454,30 +454,30 @@ export enum ErrorType {
export class OllamaError extends Error {
public readonly type: ErrorType;
&nbsp;
constructor(message: string, type: ErrorType) {
super(message);
this.type = type;
Object.setPrototypeOf(this, OllamaError.prototype);
<span class="fstat-no" title="function not covered" > constructor(m</span>essage: string, type: ErrorType) {
<span class="cstat-no" title="statement not covered" > super(message);</span>
<span class="cstat-no" title="statement not covered" > this.type = type;</span>
<span class="cstat-no" title="statement not covered" > Object.setPrototypeOf(this, OllamaError.prototype);</span>
}
}
&nbsp;
export class NetworkError extends OllamaError {
public readonly statusCode?: number;
&nbsp;
constructor(message: string, statusCode?: number) {
super(message, ErrorType.NETWORK_ERROR);
this.statusCode = statusCode;
Object.setPrototypeOf(this, NetworkError.prototype);
<span class="fstat-no" title="function not covered" > constructor(m</span>essage: string, statusCode?: number) {
<span class="cstat-no" title="statement not covered" > super(message, ErrorType.NETWORK_ERROR);</span>
<span class="cstat-no" title="statement not covered" > this.statusCode = statusCode;</span>
<span class="cstat-no" title="statement not covered" > Object.setPrototypeOf(this, NetworkError.prototype);</span>
}
}
&nbsp;
export class ApiError extends OllamaError {
public readonly statusCode?: number;
&nbsp;
constructor(message: string, statusCode?: number) {
super(message, ErrorType.API_ERROR);
this.statusCode = statusCode;
Object.setPrototypeOf(this, ApiError.prototype);
<span class="fstat-no" title="function not covered" > constructor(m</span>essage: string, statusCode?: number) {
<span class="cstat-no" title="statement not covered" > super(message, ErrorType.API_ERROR);</span>
<span class="cstat-no" title="statement not covered" > this.statusCode = statusCode;</span>
<span class="cstat-no" title="statement not covered" > Object.setPrototypeOf(this, ApiError.prototype);</span>
}
}
&nbsp;
@@ -489,37 +489,37 @@ export interface ValidationFieldDetails {
export class ValidationError extends OllamaError {
public readonly details?: ValidationFieldDetails;
&nbsp;
constructor(message: string, details?: ValidationFieldDetails) {
super(message, ErrorType.VALIDATION_ERROR);
this.details = details;
Object.setPrototypeOf(this, ValidationError.prototype);
<span class="fstat-no" title="function not covered" > constructor(m</span>essage: string, details?: ValidationFieldDetails) {
<span class="cstat-no" title="statement not covered" > super(message, ErrorType.VALIDATION_ERROR);</span>
<span class="cstat-no" title="statement not covered" > this.details = details;</span>
<span class="cstat-no" title="statement not covered" > Object.setPrototypeOf(this, ValidationError.prototype);</span>
}
}
&nbsp;
export class StreamingError extends OllamaError {
constructor(message: string) {
super(message, ErrorType.STREAMING_ERROR);
Object.setPrototypeOf(this, StreamingError.prototype);
<span class="fstat-no" title="function not covered" > constructor(m</span>essage: string) {
<span class="cstat-no" title="statement not covered" > super(message, ErrorType.STREAMING_ERROR);</span>
<span class="cstat-no" title="statement not covered" > Object.setPrototypeOf(this, StreamingError.prototype);</span>
}
}
&nbsp;
export class ToolExecutionError extends OllamaError {
public readonly toolName: string;
&nbsp;
constructor(message: string, toolName: string) {
super(message, ErrorType.TOOL_EXECUTION_ERROR);
this.toolName = toolName;
Object.setPrototypeOf(this, ToolExecutionError.prototype);
<span class="fstat-no" title="function not covered" > constructor(m</span>essage: string, toolName: string) {
<span class="cstat-no" title="statement not covered" > super(message, ErrorType.TOOL_EXECUTION_ERROR);</span>
<span class="cstat-no" title="statement not covered" > this.toolName = toolName;</span>
<span class="cstat-no" title="statement not covered" > Object.setPrototypeOf(this, ToolExecutionError.prototype);</span>
}
}
&nbsp;
export class PathValidationError extends OllamaError {
public readonly path: string;
&nbsp;
constructor(message: string, path: string) {
super(message, ErrorType.PATH_VALIDATION_ERROR);
this.path = path;
Object.setPrototypeOf(this, PathValidationError.prototype);
<span class="fstat-no" title="function not covered" > constructor(m</span>essage: string, path: string) {
<span class="cstat-no" title="statement not covered" > super(message, ErrorType.PATH_VALIDATION_ERROR);</span>
<span class="cstat-no" title="statement not covered" > this.path = path;</span>
<span class="cstat-no" title="statement not covered" > Object.setPrototypeOf(this, PathValidationError.prototype);</span>
}
}
&nbsp;
@@ -628,7 +628,7 @@ export interface VaultIndexEntry {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T19:45:32.568Z
at 2026-05-06T19:51:33.347Z
</div>
<script src="prettify.js"></script>
<script>
+164 -164
View File
@@ -23,30 +23,30 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">81.72% </span>
<span class="strong">35.48% </span>
<span class="quiet">Statements</span>
<span class='fraction'>76/93</span>
<span class='fraction'>33/93</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">70.83% </span>
<span class="strong">20.83% </span>
<span class="quiet">Branches</span>
<span class='fraction'>34/48</span>
<span class='fraction'>10/48</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">56.25% </span>
<span class="strong">25% </span>
<span class="quiet">Functions</span>
<span class='fraction'>9/16</span>
<span class='fraction'>4/16</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">82.22% </span>
<span class="strong">35.55% </span>
<span class="quiet">Lines</span>
<span class='fraction'>74/90</span>
<span class='fraction'>32/90</span>
</div>
@@ -61,7 +61,7 @@
</div>
</template>
</div>
<div class='status-line high'></div>
<div class='status-line low'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
@@ -311,8 +311,8 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">55x</span>
<span class="cline-any cline-yes">55x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -326,126 +326,126 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">109x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">108x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">108x</span>
<span class="cline-any cline-yes">54x</span>
<span class="cline-any cline-yes">54x</span>
<span class="cline-any cline-yes">79x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">54x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">38x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">37x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">34x</span>
<span class="cline-any cline-yes">34x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">30x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">29x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -460,11 +460,11 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -515,9 +515,9 @@ export class Logger {
}
}
&nbsp;
static warn(message: string, category: string = <span class="branch-0 cbranch-no" title="branch not covered" >'general')</span>: void {
if (LogLevel.WARN &gt;= Logger.minLevel) {
console.warn(`[${category}] WARN: ${message}`);
<span class="fstat-no" title="function not covered" > static </span>warn(message: string, category: string = <span class="branch-0 cbranch-no" title="branch not covered" >'general')</span>: void {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (LogLevel.WARN &gt;= Logger.minLevel) {</span>
<span class="cstat-no" title="statement not covered" > console.warn(`[${category}] WARN: ${message}`);</span>
}
}
&nbsp;
@@ -530,72 +530,72 @@ export class Logger {
&nbsp;
// ==================== URL &amp; Model Validation ====================
&nbsp;
export function validateOllamaUrl(url: string): { valid: boolean; error?: string } {
if (typeof url !== 'string' || !url.trim()) {
return { valid: false, error: 'URL cannot be empty' };
export function <span class="fstat-no" title="function not covered" >validateOllamaUrl(</span>url: string): { valid: boolean; error?: string } {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (typeof url !== 'string' || !url.trim()) {</span>
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'URL cannot be empty' };</span>
}
&nbsp;
const trimmedUrl = url.trim();
const trimmedUrl = <span class="cstat-no" title="statement not covered" >url.trim();</span>
&nbsp;
if (trimmedUrl.endsWith('/')) {
return { valid: false, error: 'URL should not end with a slash' };
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (trimmedUrl.endsWith('/')) {</span>
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'URL should not end with a slash' };</span>
}
&nbsp;
try {
const parsed = new URL(trimmedUrl);
<span class="missing-if-branch" title="if path not taken" >I</span>if (parsed.protocol !== 'http:' &amp;&amp; parsed.protocol !== 'https:') {
<span class="cstat-no" title="statement not covered" > try {</span>
const parsed = <span class="cstat-no" title="statement not covered" >new URL(trimmedUrl);</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (parsed.protocol !== 'http:' &amp;&amp; parsed.protocol !== 'https:') {</span>
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' };</span>
}
return { valid: true };
<span class="cstat-no" title="statement not covered" > return { valid: true };</span>
} catch {
return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' };
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' };</span>
}
}
&nbsp;
export function validateModelName(model: string): { valid: boolean; error?: string } {
if (typeof model !== 'string') {
return { valid: false, error: 'Model name must be a string' };
export function <span class="fstat-no" title="function not covered" >validateModelName(</span>model: string): { valid: boolean; error?: string } {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (typeof model !== 'string') {</span>
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Model name must be a string' };</span>
}
&nbsp;
const trimmedModel = model.trim();
const trimmedModel = <span class="cstat-no" title="statement not covered" >model.trim();</span>
&nbsp;
// Explicit check for empty string after trimming
if (!trimmedModel || trimmedModel.length === 0) {
return { valid: false, error: 'Model name cannot be empty' };
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!trimmedModel || trimmedModel.length === 0) {</span>
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Model name cannot be empty' };</span>
}
&nbsp;
if (trimmedModel.length &lt; 2) {
return { valid: false, error: 'Model name must be at least 2 characters long' };
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (trimmedModel.length &lt; 2) {</span>
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Model name must be at least 2 characters long' };</span>
}
&nbsp;
if (trimmedModel.length &gt; 100) {
return { valid: false, error: 'Model name must be less than 100 characters long' };
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (trimmedModel.length &gt; 100) {</span>
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Model name must be less than 100 characters long' };</span>
}
&nbsp;
if (!/^[a-zA-Z0-9._:-]+$/.test(trimmedModel)) {
return {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!/^[a-zA-Z0-9._:-]+$/.test(trimmedModel)) {</span>
<span class="cstat-no" title="statement not covered" > return {</span>
valid: false,
error: 'Model name can only contain letters, numbers, dots, dashes, underscores, and colons',
};
}
&nbsp;
return { valid: true };
<span class="cstat-no" title="statement not covered" > return { valid: true };</span>
}
&nbsp;
export function validatePluginSettings(settings: { ollamaUrl: string; model: string }): string[] {
const errors: string[] = [];
export function <span class="fstat-no" title="function not covered" >validatePluginSettings(</span>settings: { ollamaUrl: string; model: string }): string[] {
const errors: string[] = <span class="cstat-no" title="statement not covered" >[];</span>
&nbsp;
const urlValidation = validateOllamaUrl(settings.ollamaUrl);
if (!urlValidation.valid) {
errors.push(`Invalid Ollama URL: ${urlValidation.error}`);
const urlValidation = <span class="cstat-no" title="statement not covered" >validateOllamaUrl(settings.ollamaUrl);</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!urlValidation.valid) {</span>
<span class="cstat-no" title="statement not covered" > errors.push(`Invalid Ollama URL: ${urlValidation.error}`);</span>
}
&nbsp;
const modelValidation = validateModelName(settings.model);
if (!modelValidation.valid) {
errors.push(`Invalid Model Name: ${modelValidation.error}`);
const modelValidation = <span class="cstat-no" title="statement not covered" >validateModelName(settings.model);</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!modelValidation.valid) {</span>
<span class="cstat-no" title="statement not covered" > errors.push(`Invalid Model Name: ${modelValidation.error}`);</span>
}
&nbsp;
return errors;
<span class="cstat-no" title="statement not covered" > return errors;</span>
}
&nbsp;
// ==================== Safe JSON Parsing ====================
@@ -604,50 +604,50 @@ const MAX_JSON_SIZE = 1_000_000;
const MAX_JSON_NESTING = 24;
&nbsp;
function countNestingDepth(value: unknown, depth: number = 0): number {
if (depth &gt; MAX_JSON_NESTING) {
return depth;
<span class="missing-if-branch" title="if path not taken" >I</span>if (depth &gt; MAX_JSON_NESTING) {
<span class="cstat-no" title="statement not covered" > return depth;</span>
}
<span class="missing-if-branch" title="if path not taken" >I</span>if (Array.isArray(value)) {
<span class="cstat-no" title="statement not covered" > return Math.max(...value.map(<span class="fstat-no" title="function not covered" >(i</span>tem) =&gt; <span class="cstat-no" title="statement not covered" >countNestingDepth(item, depth + 1))</span>, depth);</span>
}
if (value !== null &amp;&amp; typeof value === 'object') {
const entries = Object.values(value as Record&lt;string, unknown&gt;);
if (entries.length === 0) return depth;
<span class="missing-if-branch" title="if path not taken" >I</span>if (entries.length === 0) <span class="cstat-no" title="statement not covered" >return depth;</span>
return Math.max(...entries.map((item) =&gt; countNestingDepth(item, depth + 1)), depth);
}
return depth;
}
&nbsp;
export function safeParseJson(jsonString: string): unknown {
if (typeof jsonString !== 'string') {
throw new Error('Input must be a string');
<span class="missing-if-branch" title="if path not taken" >I</span>if (typeof jsonString !== 'string') {
<span class="cstat-no" title="statement not covered" > throw new Error('Input must be a string');</span>
}
&nbsp;
if (jsonString.length &gt; MAX_JSON_SIZE) {
throw new Error('JSON input too large');
<span class="missing-if-branch" title="if path not taken" >I</span>if (jsonString.length &gt; MAX_JSON_SIZE) {
<span class="cstat-no" title="statement not covered" > throw new Error('JSON input too large');</span>
}
&nbsp;
let parsed: unknown;
try {
parsed = JSON.parse(jsonString);
} catch {
throw new Error('Invalid JSON');
<span class="cstat-no" title="statement not covered" > throw new Error('Invalid JSON');</span>
}
&nbsp;
// Check for dangerous prototype pollution patterns
const reStringified = JSON.stringify(parsed);
if (
<span class="missing-if-branch" title="if path not taken" >I</span>if (
reStringified.includes('constructor') ||
reStringified.includes('prototype') ||
reStringified.includes('__proto__') ||
reStringified.includes('function')
) {
throw new Error('dangerous code pattern detected');
<span class="cstat-no" title="statement not covered" > throw new Error('dangerous code pattern detected');</span>
}
&nbsp;
// Check nesting depth
if (countNestingDepth(parsed) &gt; MAX_JSON_NESTING) {
throw new Error('JSON nesting too deep');
<span class="missing-if-branch" title="if path not taken" >I</span>if (countNestingDepth(parsed) &gt; MAX_JSON_NESTING) {
<span class="cstat-no" title="statement not covered" > throw new Error('JSON nesting too deep');</span>
}
&nbsp;
return parsed;
@@ -664,12 +664,12 @@ export function <span class="fstat-no" title="function not covered" >sanitizeFil
&nbsp;
// ==================== HTTP Helpers ====================
&nbsp;
export function isValidHttpUrl(url: string): boolean {
try {
const parsed = new URL(url);
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
export function <span class="fstat-no" title="function not covered" >isValidHttpUrl(</span>url: string): boolean {
<span class="cstat-no" title="statement not covered" > try {</span>
const parsed = <span class="cstat-no" title="statement not covered" >new URL(url);</span>
<span class="cstat-no" title="statement not covered" > return parsed.protocol === 'http:' || parsed.protocol === 'https:';</span>
} catch {
return false;
<span class="cstat-no" title="statement not covered" > return false;</span>
}
}
&nbsp;
@@ -685,7 +685,7 @@ export function <span class="fstat-no" title="function not covered" >convertMark
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T19:45:32.568Z
at 2026-05-06T19:51:33.347Z
</div>
<script src="prettify.js"></script>
<script>
+258 -258
View File
@@ -23,30 +23,30 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">92.1% </span>
<span class="strong">17.54% </span>
<span class="quiet">Statements</span>
<span class='fraction'>105/114</span>
<span class='fraction'>20/114</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">78.26% </span>
<span class="strong">8.69% </span>
<span class="quiet">Branches</span>
<span class='fraction'>36/46</span>
<span class='fraction'>4/46</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="strong">33.33% </span>
<span class="quiet">Functions</span>
<span class='fraction'>15/15</span>
<span class='fraction'>5/15</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">94.33% </span>
<span class="strong">17.92% </span>
<span class="quiet">Lines</span>
<span class='fraction'>100/106</span>
<span class='fraction'>19/106</span>
</div>
@@ -61,7 +61,7 @@
</div>
</template>
</div>
<div class='status-line high'></div>
<div class='status-line low'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
@@ -370,142 +370,142 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">62x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">62x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-yes">38x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">54x</span>
<span class="cline-any cline-yes">54x</span>
<span class="cline-any cline-yes">53x</span>
<span class="cline-any cline-yes">53x</span>
<span class="cline-any cline-yes">53x</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">54x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">95x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">95x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">362x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">60x</span>
<span class="cline-any cline-yes">60x</span>
<span class="cline-any cline-yes">60x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">60x</span>
<span class="cline-any cline-yes">60x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -514,25 +514,25 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">60x</span>
<span class="cline-any cline-yes">60x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">60x</span>
<span class="cline-any cline-yes">60x</span>
<span class="cline-any cline-yes">60x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">60x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">60x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">60x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -540,56 +540,56 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">58x</span>
<span class="cline-any cline-yes">58x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">58x</span>
<span class="cline-any cline-yes">64x</span>
<span class="cline-any cline-yes">64x</span>
<span class="cline-any cline-yes">64x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">64x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">64x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">64x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">64x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">64x</span>
<span class="cline-any cline-yes">58x</span>
<span class="cline-any cline-yes">58x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">64x</span>
<span class="cline-any cline-yes">56x</span>
<span class="cline-any cline-yes">56x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">64x</span>
<span class="cline-any cline-yes">60x</span>
<span class="cline-any cline-yes">60x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">58x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -597,16 +597,16 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">127x</span>
<span class="cline-any cline-yes">127x</span>
<span class="cline-any cline-yes">126x</span>
<span class="cline-any cline-yes">126x</span>
<span class="cline-any cline-yes">122x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">63x</span>
<span class="cline-any cline-yes">63x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -651,8 +651,8 @@ class VaultIndexer {
}
&nbsp;
async searchVault(query: string, limit: number = <span class="branch-0 cbranch-no" title="branch not covered" >5)</span>: Promise&lt;VaultIndexEntry[]&gt; {
if (!query || !query.trim()) {
return [];
<span class="missing-if-branch" title="if path not taken" >I</span>if (!query || !query.trim()) {
<span class="cstat-no" title="statement not covered" > return [];</span>
}
&nbsp;
<span class="missing-if-branch" title="if path not taken" >I</span>if (!this.vault) {
@@ -665,8 +665,8 @@ class VaultIndexer {
const results = await this.processFilesInBatches(vault, allFiles, queryTokens);
&nbsp;
return results
.filter((result): result is NonNullable&lt;typeof result&gt; =&gt; result !== null)
.sort((a, b) =&gt; b.score - a.score)
.filter(<span class="fstat-no" title="function not covered" >(r</span>esult): result is NonNullable&lt;typeof result&gt; =&gt; <span class="cstat-no" title="statement not covered" >result !== null)</span>
.sort(<span class="fstat-no" title="function not covered" >(a</span>, b) =&gt; <span class="cstat-no" title="statement not covered" >b.score - a.score)</span>
.slice(0, limit);
}
&nbsp;
@@ -680,43 +680,43 @@ class VaultIndexer {
const seenPaths = new Set&lt;string&gt;();
&nbsp;
for (let i = 0; i &lt; files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(async (file) =&gt; {
try {
const content = await vault.read(file);
const tokenized = this.tokenizeContent(content);
const scoreResult = this.calculateWeightedScore(tokenized, queryTokens, file);
if (scoreResult.score &gt; 0) {
const entry: VaultIndexEntry = {
const batch = <span class="cstat-no" title="statement not covered" >files.slice(i, i + batchSize);</span>
const batchResults = <span class="cstat-no" title="statement not covered" >await Promise.all(</span>
batch.map(<span class="fstat-no" title="function not covered" >async </span>(file) =&gt; {
<span class="cstat-no" title="statement not covered" > try {</span>
const content = <span class="cstat-no" title="statement not covered" >await vault.read(file);</span>
const tokenized = <span class="cstat-no" title="statement not covered" >this.tokenizeContent(content);</span>
const scoreResult = <span class="cstat-no" title="statement not covered" >this.calculateWeightedScore(tokenized, queryTokens, file);</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (scoreResult.score &gt; 0) {</span>
const entry: VaultIndexEntry = <span class="cstat-no" title="statement not covered" >{</span>
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;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!seenPaths.has(entry.path)) {</span>
<span class="cstat-no" title="statement not covered" > seenPaths.add(entry.path);</span>
<span class="cstat-no" title="statement not covered" > return entry;</span>
}
<span class="cstat-no" title="statement not covered" > return null;</span>
}
return null;
<span class="cstat-no" title="statement not covered" > return null;</span>
} catch (error) {
Logger.warn(
`Failed to process ${file.path}: ${error instanceof Error ? error.message : <span class="branch-1 cbranch-no" title="branch not covered" >String(error)}</span>`,
<span class="cstat-no" title="statement not covered" > Logger.warn(</span>
`Failed to process ${file.path}: ${error instanceof Error ? error.message : String(error)}`,
'vault-indexer'
);
return null;
<span class="cstat-no" title="statement not covered" > return null;</span>
}
})
);
&nbsp;
const validResults = batchResults.filter(
(result): result is NonNullable&lt;typeof result&gt; =&gt; result !== null
const validResults = <span class="cstat-no" title="statement not covered" >batchResults.filter(</span>
<span class="fstat-no" title="function not covered" > (r</span>esult): result is NonNullable&lt;typeof result&gt; =&gt; <span class="cstat-no" title="statement not covered" >result !== null</span>
);
results.push(...validResults);
<span class="cstat-no" title="statement not covered" > results.push(...validResults);</span>
&nbsp;
<span class="missing-if-branch" title="if path not taken" >I</span>if (results.length &gt;= 50) {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (results.length &gt;= 50) {</span>
<span class="cstat-no" title="statement not covered" > break;</span>
}
}
@@ -758,28 +758,28 @@ class VaultIndexer {
.filter((token) =&gt; token.length &gt; 1 &amp;&amp; !stopWords.has(token));
}
&nbsp;
private tokenizeContent(content: string): TokenizedContent {
const tokens: string[] = [];
const headings: string[] = [];
const frontmatter: Frontmatter = {};
private <span class="fstat-no" title="function not covered" >tokenizeContent(</span>content: string): TokenizedContent {
const tokens: string[] = <span class="cstat-no" title="statement not covered" >[];</span>
const headings: string[] = <span class="cstat-no" title="statement not covered" >[];</span>
const frontmatter: Frontmatter = <span class="cstat-no" title="statement not covered" >{};</span>
let firstParagraph: string | undefined;
&nbsp;
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(':');
<span class="missing-if-branch" title="if path not taken" >I</span>if (!key) <span class="cstat-no" title="statement not covered" >continue;</span>
const value = valueParts.join(':').trim();
if (key.trim() === 'title') {
if (value) {
frontmatter.title = value;
const frontmatterMatch = <span class="cstat-no" title="statement not covered" >content.match(/^---(.*?)---/s);</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (frontmatterMatch) {</span>
<span class="cstat-no" title="statement not covered" > try {</span>
const frontmatterContent = <span class="cstat-no" title="statement not covered" >frontmatterMatch[1];</span>
const lines = <span class="cstat-no" title="statement not covered" >frontmatterContent.trim().split('\n');</span>
<span class="cstat-no" title="statement not covered" > for (const line of lines) {</span>
const [key, ...valueParts] = <span class="cstat-no" title="statement not covered" >line.split(':');</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!key) <span class="cstat-no" title="statement not covered" >continue;</span></span>
const value = <span class="cstat-no" title="statement not covered" >valueParts.join(':').trim();</span>
<span class="cstat-no" title="statement not covered" > if (key.trim() === 'title') {</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (value) {</span>
<span class="cstat-no" title="statement not covered" > frontmatter.title = value;</span>
}
} else if (key.trim() === 'tags') {
if (value) {
frontmatter.tags = value;
} else <span class="cstat-no" title="statement not covered" ><span class="missing-if-branch" title="if path not taken" >I</span>if (key.trim() === 'tags') {</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (value) {</span>
<span class="cstat-no" title="statement not covered" > frontmatter.tags = value;</span>
}
}
}
@@ -788,99 +788,99 @@ class VaultIndexer {
}
}
&nbsp;
const headingMatches = content.match(/^#{1,6} (.*?)$/gm);
if (headingMatches) {
headings.push(...headingMatches.map((h: string) =&gt; h.replace(/^#{1,6} /, '')));
const headingMatches = <span class="cstat-no" title="statement not covered" >content.match(/^#{1,6} (.*?)$/gm);</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (headingMatches) {</span>
<span class="cstat-no" title="statement not covered" > headings.push(...headingMatches.map(<span class="fstat-no" title="function not covered" >(h</span>: string) =&gt; <span class="cstat-no" title="statement not covered" >h.replace(/^#{1,6} /, ''))</span>);</span>
}
&nbsp;
const paragraphMatch = content.match(/^([^#]*?)(?=#|\n\n|$)/s);
if (paragraphMatch) {
firstParagraph = paragraphMatch[1].trim();
const paragraphMatch = <span class="cstat-no" title="statement not covered" >content.match(/^([^#]*?)(?=#|\n\n|$)/s);</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (paragraphMatch) {</span>
<span class="cstat-no" title="statement not covered" > firstParagraph = paragraphMatch[1].trim();</span>
}
&nbsp;
const allText = content
const allText = <span class="cstat-no" title="statement not covered" >content</span>
.replace(/^---.*?---/s, '')
.replace(/^#.*?$/gm, '')
.replace(/```.*?```/gs, '')
.replace(/`.*?`/g, '')
.replace(/\[.*?\]\(.*?\)/g, '');
tokens.push(...this.tokenize(allText));
<span class="cstat-no" title="statement not covered" > tokens.push(...this.tokenize(allText));</span>
&nbsp;
return { tokens, headings, frontmatter, firstParagraph };
<span class="cstat-no" title="statement not covered" > return { tokens, headings, frontmatter, firstParagraph };</span>
}
&nbsp;
private calculateWeightedScore(
private <span class="fstat-no" title="function not covered" >calculateWeightedScore(</span>
tokenized: TokenizedContent,
queryTokens: string[],
file?: VaultFile
): ScoreResult {
let totalScore = 0;
const matchedTokens: Set&lt;string&gt; = new Set&lt;string&gt;();
let totalScore = <span class="cstat-no" title="statement not covered" >0;</span>
const matchedTokens: Set&lt;string&gt; = <span class="cstat-no" title="statement not covered" >new Set&lt;string&gt;();</span>
&nbsp;
for (const queryToken of queryTokens) {
let tokenScore = 0;
const stemmed = this.stemToken(queryToken);
let matched = false;
<span class="cstat-no" title="statement not covered" > for (const queryToken of queryTokens) {</span>
let tokenScore = <span class="cstat-no" title="statement not covered" >0;</span>
const stemmed = <span class="cstat-no" title="statement not covered" >this.stemToken(queryToken);</span>
let matched = <span class="cstat-no" title="statement not covered" >false;</span>
&nbsp;
<span class="missing-if-branch" title="if path not taken" >I</span>if (
<span class="cstat-no" title="statement not covered" > if (</span>
tokenized.frontmatter?.title &amp;&amp;
<span class="branch-1 cbranch-no" title="branch not covered" > this.exactMatch(tokenized.frontmatter.title, queryToken)</span>
this.exactMatch(tokenized.frontmatter.title, queryToken)
) {
<span class="cstat-no" title="statement not covered" > tokenScore += 3;</span>
<span class="cstat-no" title="statement not covered" > matched = true;</span>
} else if (
} else <span class="cstat-no" title="statement not covered" ><span class="missing-if-branch" title="if path not taken" >I</span>if (</span>
file &amp;&amp;
file.basename &amp;&amp;
this.exactMatch(file.basename.replace(/\.md$/, ''), queryToken)
) {
tokenScore += 3;
matched = true;
<span class="cstat-no" title="statement not covered" > tokenScore += 3;</span>
<span class="cstat-no" title="statement not covered" > matched = true;</span>
}
&nbsp;
if (tokenized.frontmatter?.tags &amp;&amp; this.exactMatch(tokenized.frontmatter.tags, queryToken)) {
tokenScore += 2.5;
matched = true;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (tokenized.frontmatter?.tags &amp;&amp; this.exactMatch(tokenized.frontmatter.tags, queryToken)) {</span>
<span class="cstat-no" title="statement not covered" > tokenScore += 2.5;</span>
<span class="cstat-no" title="statement not covered" > matched = true;</span>
}
&nbsp;
if (tokenized.headings.some((heading: string) =&gt; heading.toLowerCase().includes(stemmed))) {
tokenScore += 5;
matched = true;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (tokenized.headings.some(<span class="fstat-no" title="function not covered" >(h</span>eading: string) =&gt; <span class="cstat-no" title="statement not covered" >heading.toLowerCase().includes(stemmed))</span>) {</span>
<span class="cstat-no" title="statement not covered" > tokenScore += 5;</span>
<span class="cstat-no" title="statement not covered" > matched = true;</span>
}
&nbsp;
if (tokenized.firstParagraph &amp;&amp; tokenized.firstParagraph.toLowerCase().includes(stemmed)) {
tokenScore += 1.5;
matched = true;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (tokenized.firstParagraph &amp;&amp; tokenized.firstParagraph.toLowerCase().includes(stemmed)) {</span>
<span class="cstat-no" title="statement not covered" > tokenScore += 1.5;</span>
<span class="cstat-no" title="statement not covered" > matched = true;</span>
}
&nbsp;
if (tokenized.tokens.includes(stemmed)) {
tokenScore += 1;
matched = true;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (tokenized.tokens.includes(stemmed)) {</span>
<span class="cstat-no" title="statement not covered" > tokenScore += 1;</span>
<span class="cstat-no" title="statement not covered" > matched = true;</span>
}
&nbsp;
if (matched) {
totalScore += tokenScore;
matchedTokens.add(queryToken);
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (matched) {</span>
<span class="cstat-no" title="statement not covered" > totalScore += tokenScore;</span>
<span class="cstat-no" title="statement not covered" > matchedTokens.add(queryToken);</span>
}
}
&nbsp;
return {
<span class="cstat-no" title="statement not covered" > return {</span>
score: totalScore,
matchedFields: Array.from(matchedTokens),
};
}
&nbsp;
private stemToken(token: string): string {
private <span class="fstat-no" title="function not covered" >stemToken(</span>token: string): string {
// Improved stemmer that handles edge cases
<span class="missing-if-branch" title="if path not taken" >I</span>if (token.length &lt;= 3) <span class="cstat-no" title="statement not covered" >return token; </span>// Don't stem very short tokens
if (token.endsWith('s')) return token.slice(0, -1);
<span class="missing-if-branch" title="if path not taken" >I</span>if (token.endsWith('ed') &amp;&amp; <span class="branch-1 cbranch-no" title="branch not covered" >token.length &gt; 4)</span> <span class="cstat-no" title="statement not covered" >return token.slice(0, -2); </span>// Don't stem 3-letter words ending in ed
if (token.endsWith('ing') &amp;&amp; token.length &gt; 5) return token.slice(0, -3); // Don't stem 4-letter words ending in ing
return token;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (token.length &lt;= 3) <span class="cstat-no" title="statement not covered" >return token; </span></span>// Don't stem very short tokens
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (token.endsWith('s')) <span class="cstat-no" title="statement not covered" >return token.slice(0, -1);</span></span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (token.endsWith('ed') &amp;&amp; token.length &gt; 4) <span class="cstat-no" title="statement not covered" >return token.slice(0, -2); </span></span>// Don't stem 3-letter words ending in ed
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (token.endsWith('ing') &amp;&amp; token.length &gt; 5) <span class="cstat-no" title="statement not covered" >return token.slice(0, -3); </span></span>// Don't stem 4-letter words ending in ing
<span class="cstat-no" title="statement not covered" > return token;</span>
}
&nbsp;
private exactMatch(content: string, token: string): boolean {
const stemmedToken = this.stemToken(token);
return content.toLowerCase().includes(stemmedToken);
private <span class="fstat-no" title="function not covered" >exactMatch(</span>content: string, token: string): boolean {
const stemmedToken = <span class="cstat-no" title="statement not covered" >this.stemToken(token);</span>
<span class="cstat-no" title="statement not covered" > return content.toLowerCase().includes(stemmedToken);</span>
}
}
&nbsp;
@@ -892,7 +892,7 @@ export { VaultIndexer };
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T19:45:32.568Z
at 2026-05-06T19:51:33.347Z
</div>
<script src="prettify.js"></script>
<script>
Executable → Regular
+822 -822
View File
File diff suppressed because it is too large Load Diff
-405
View File
@@ -1,405 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChatView = void 0;
const obsidian_1 = require("obsidian");
const DEFAULT_VAULT_SEARCH_LIMIT = 3;
const MAX_MESSAGE_HISTORY = 50;
const MAX_STREAM_CHUNKS = 1000;
const ollama_client_1 = require("./ollama-client");
const vault_indexer_1 = require("./vault-indexer");
const tool_executor_1 = require("./tool-executor");
const error_handler_1 = require("./error-handler");
class ChatView extends obsidian_1.ItemView {
// Getters for testing
getSendButtonClickHandler() {
return this.sendButtonClickHandler;
}
getInputKeyDownHandler() {
return this.inputKeyDownHandler;
}
getNewChatButtonClickHandler() {
return this.newChatButtonClickHandler;
}
constructor(leaf, settings) {
super(leaf);
this.messages = [];
this.lastMessageEl = null;
this.newChatButton = null;
this.sendButton = null;
this.inputEl = null;
this.chatContainer = null;
this.sendButtonClickHandler = null;
this.inputKeyDownHandler = null;
this.newChatButtonClickHandler = null;
this.sendButtonEventHandler = null;
this.inputKeyDownEventHandler = null;
this.newChatButtonEventHandler = null;
this.listenersAttached = false;
this.settings = settings;
this.ollamaClient = new ollama_client_1.OllamaClient(settings.ollamaUrl, settings.model);
this.vaultIndexer = new vault_indexer_1.VaultIndexer(this.app.vault);
this.toolExecutor = new tool_executor_1.ToolExecutor(this.app.vault, this.app);
}
updateSettings(newSettings) {
this.settings = newSettings;
this.ollamaClient = new ollama_client_1.OllamaClient(newSettings.ollamaUrl, newSettings.model);
}
getViewType() {
return 'ollama-chat-view';
}
getDisplayText() {
return 'Ollama Chat';
}
onOpen() {
this.render();
this.removeEventListeners(); // Clean up any existing listeners before reattaching
this.setupEventListeners();
return Promise.resolve();
}
onSettingsChange(newSettings) {
this.updateSettings(newSettings);
}
onClose() {
this.ollamaClient.cancelStream();
this.removeEventListeners();
this.cleanupStreamingResources();
this.lastMessageEl = null;
this.sendButton = null;
this.inputEl = null;
this.chatContainer = null;
return Promise.resolve();
}
cleanupStreamingResources() {
// Ensure any ongoing streaming is properly cleaned up
if (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}`,
});
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);
}
}
}
setupEventListeners() {
if (!this.sendButton || !this.inputEl || this.listenersAttached)
return;
// Create handlers if they don't exist
if (!this.sendButtonClickHandler) {
this.sendButtonClickHandler = async () => {
if (!this.inputEl)
return;
await this.handleUserInput(this.inputEl.value);
this.inputEl.value = '';
};
}
if (!this.inputKeyDownHandler) {
this.inputKeyDownHandler = async (e) => {
if (!this.inputEl || e.key !== 'Enter' || e.shiftKey)
return;
e.preventDefault();
await this.handleUserInput(this.inputEl.value);
this.inputEl.value = '';
};
}
// Add event listeners
this.sendButtonEventHandler = () => {
void this.sendButtonClickHandler?.();
};
this.inputKeyDownEventHandler = (e) => {
void this.inputKeyDownHandler?.(e);
};
this.sendButton.addEventListener('click', this.sendButtonEventHandler);
this.inputEl.addEventListener('keydown', this.inputKeyDownEventHandler);
if (this.newChatButton) {
if (!this.newChatButtonClickHandler) {
this.newChatButtonClickHandler = () => this.clearConversation();
}
this.newChatButtonEventHandler = () => {
this.newChatButtonClickHandler?.();
};
this.newChatButton.addEventListener('click', this.newChatButtonEventHandler);
}
this.listenersAttached = true;
}
removeEventListeners() {
if (this.sendButton && this.sendButtonEventHandler) {
this.sendButton.removeEventListener('click', this.sendButtonEventHandler);
}
if (this.inputEl && this.inputKeyDownEventHandler) {
this.inputEl.removeEventListener('keydown', this.inputKeyDownEventHandler);
}
if (this.newChatButton && this.newChatButtonEventHandler) {
this.newChatButton.removeEventListener('click', this.newChatButtonEventHandler);
}
this.sendButtonEventHandler = null;
this.inputKeyDownEventHandler = null;
this.newChatButtonEventHandler = null;
this.listenersAttached = false;
}
clearConversation() {
// Create new array to ensure immutability
this.messages = [];
this.lastMessageEl = null;
this.render();
new obsidian_1.Notice('Conversation cleared');
}
updateMessageById(id, partial) {
const index = this.messages.findIndex((m) => m.id === id);
if (index < 0)
return false;
this.messages = [
...this.messages.slice(0, index),
{ ...this.messages[index], ...partial },
...this.messages.slice(index + 1),
];
return true;
}
updateLastMessage(content) {
const streamingMessage = this.messages.find((msg) => msg.isStreaming);
if (streamingMessage && !this.lastMessageEl) {
this.lastMessageEl = this.contentEl.createEl('div', {
cls: `ollama-message assistant`,
});
this.lastMessageEl.setAttribute('data-msg-id', streamingMessage.id);
}
if (this.lastMessageEl) {
this.lastMessageEl.textContent = content;
}
}
async handleUserInput(content) {
if (!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;
if (context.length > MAX_CONTEXT_LENGTH) {
context = context.substring(0, MAX_CONTEXT_LENGTH) + '\n\n... (truncated)';
}
const systemMessage = {
role: 'system',
content: 'You are a helpful assistant.',
};
const userContent = context ? `${context}\n\n${userMessage}` : userMessage;
const userMessageWithContext = {
role: 'user',
content: userContent,
};
const messages = [
systemMessage,
...this.messages.map((m) => ({
role: m.role,
content: m.content,
tool_calls: m.tool_calls,
})),
userMessageWithContext,
];
const tools = [
{
type: 'function',
function: {
name: 'create_file',
description: 'Create a new file in the vault',
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: "Relative path within the vault, e.g. 'Notes/todo.md'",
},
content: { type: 'string', 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 = {
id: userMessageId,
role: 'user',
content: userMessage,
timestamp: Date.now(),
};
const assistantMessage = {
id: assistantMessageId,
role: 'assistant',
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 = [];
let chunkCount = 0;
for await (const chunk of stream) {
chunkCount++;
if (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;
if (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 = [];
for (const result of settledResults) {
if (result.status === 'fulfilled') {
toolResults.push(result.value);
}
else {
// Use centralized error handler for tool errors
error_handler_1.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 = [
...messages,
{ role: 'assistant', content: fullResponse, tool_calls: toolCalls },
...toolResults.map((result) => ({
role: 'tool',
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 last message immutably — only if no tool calls were processed
if (toolCalls.length === 0) {
const lastMessageIndex = this.messages.length - 1;
if (lastMessageIndex >= 0) {
const lastMessage = { ...this.messages[lastMessageIndex], isStreaming: false };
this.messages = [...this.messages.slice(0, lastMessageIndex), lastMessage];
}
}
// 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
error_handler_1.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;
}
}
}
}
exports.ChatView = ChatView;
-13
View File
@@ -1,13 +0,0 @@
"use strict";
// Default plugin settings
Object.defineProperty(exports, "__esModule", { value: true });
exports.MODEL_NAME_REGEX = exports.DEFAULT_SETTINGS = void 0;
exports.DEFAULT_SETTINGS = {
ollamaUrl: 'http://localhost:11434',
model: 'llama3',
vaultSearchLimit: 3,
maxMessageHistory: 50,
lastIndexTime: 0,
};
// Model validation regex - lowercase letters, numbers, dashes, underscores only
exports.MODEL_NAME_REGEX = /^[a-z0-9-_]+$/;
-107
View File
@@ -1,107 +0,0 @@
"use strict";
// src/error-handler.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.ErrorHandler = void 0;
const obsidian_1 = require("obsidian");
const types_1 = require("./types");
class ErrorHandler {
static handleError(error, context) {
const message = this.getUserFriendlyMessage(error);
new obsidian_1.Notice(message);
if (error instanceof Error) {
const ctx = context ? ` [${context}]` : '';
// Use console.error instead of ErrorHandler.error for fatal errors
console.error(`Ollama Plugin Error${ctx}: ${error.message}`);
if (error.stack) {
console.error(error.stack);
}
}
else {
const ctx = context ? ` [${context}]` : '';
console.error(`Ollama Plugin Error${ctx}:`, error);
}
}
static getUserFriendlyMessage(error) {
if (error instanceof types_1.OllamaError) {
return this.getUserFriendlyMessageFromOllamaError(error);
}
if (error instanceof Error) {
return this.getUserFriendlyMessageFromError(error);
}
return 'An unexpected error occurred';
}
static getUserFriendlyMessageFromOllamaError(error) {
switch (error.type) {
case types_1.ErrorType.NETWORK_ERROR:
return 'Connection error. Please check if Ollama is running.';
case types_1.ErrorType.API_ERROR:
return `API error: ${error.message}`;
case types_1.ErrorType.VALIDATION_ERROR:
return this.getUserFriendlyValidationMessage(error);
case types_1.ErrorType.STREAMING_ERROR:
return 'Response too long. Please try a shorter request.';
case types_1.ErrorType.TOOL_EXECUTION_ERROR:
return `Tool error for ${error.toolName}. ${error.message}`;
case types_1.ErrorType.PATH_VALIDATION_ERROR:
return `Invalid file path: ${error.path}`;
case types_1.ErrorType.UNKNOWN_ERROR:
return 'An unexpected error occurred';
default:
return 'An unexpected error occurred';
}
}
static getUserFriendlyValidationMessage(error) {
if (error instanceof types_1.ValidationError && error.details?.field) {
const fieldMsg = error.details.field.charAt(0).toUpperCase() + error.details.field.slice(1);
return `Invalid ${fieldMsg.toLowerCase()}. ${error.details.message ?? error.message}`;
}
return 'Input validation error. Please correct your input.';
}
static getUserFriendlyMessageFromError(error) {
const msg = error.message.toLowerCase();
// Check timeout BEFORE network (more specific matches first)
if (msg.includes('timeout') || msg.includes('timed out') || msg.includes('time out')) {
return 'Request timed out. Please check your Ollama connection.';
}
if (msg.includes('network') || msg.includes('connection') || msg.includes('fetch')) {
return 'Connection error. Please check if Ollama is running.';
}
if (msg.includes('validation') || msg.includes('invalid')) {
return 'Invalid input. Please correct your input.';
}
if (msg.includes('stream') || msg.includes('chunk')) {
return 'Response too long. Please try a shorter request.';
}
if (msg.includes('tool') || msg.includes('function')) {
return 'Tool error. Please try again.';
}
if (msg.includes('path') || msg.includes('file')) {
return 'Invalid file path. Please check the path and try again.';
}
return 'An unexpected error occurred';
}
// -- Factory methods --
static createNetworkError(message, statusCode) {
return new types_1.NetworkError(message, statusCode);
}
static createApiError(message, statusCode) {
return new types_1.ApiError(message, statusCode);
}
static createValidationError(message, field) {
const details = field ? { field, message } : undefined;
return new types_1.ValidationError(message, details);
}
static createStreamingError(message) {
return new types_1.StreamingError(message);
}
static createToolExecutionError(message, toolName) {
return new types_1.ToolExecutionError(message, toolName ?? 'unknown');
}
static createPathValidationError(message, path) {
return new types_1.PathValidationError(message, path ?? '');
}
static createUnknownError(message) {
return new types_1.OllamaError(message, types_1.ErrorType.UNKNOWN_ERROR);
}
}
exports.ErrorHandler = ErrorHandler;
-134
View File
@@ -1,134 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const obsidian_1 = require("obsidian");
const chat_view_1 = require("./chat-view");
const utils_1 = require("./utils");
const constants_1 = require("./constants");
const error_handler_1 = require("./error-handler");
class OllamaPlugin extends obsidian_1.Plugin {
constructor() {
super(...arguments);
this.settings = constants_1.DEFAULT_SETTINGS;
}
async onload() {
// Initialize logging
utils_1.Logger.info('Ollama Plugin loading...', 'plugin');
await this.loadSettings();
utils_1.Logger.info('Plugin loaded successfully', 'plugin');
try {
this.registerView('ollama-chat-view', (leaf) => new chat_view_1.ChatView(leaf, this.settings));
}
catch (error) {
utils_1.Logger.error('Failed to register view: ' + error.message, 'plugin');
new obsidian_1.Notice('Failed to register Ollama chat view');
// Don't throw - let the plugin continue loading other features
}
try {
this.addRibbonIcon('message-square', 'Ollama Chat', async () => {
const leaf = this.app.workspace.getLeaf();
await leaf.setViewState({
type: 'ollama-chat-view',
active: true,
});
await this.app.workspace.revealLeaf(leaf);
});
}
catch (error) {
utils_1.Logger.error('Failed to add ribbon icon: ' + error.message, 'plugin');
new obsidian_1.Notice('Failed to add Ollama ribbon icon');
// Don't throw - let the plugin continue loading other features
}
this.addSettingTab(new OllamaSettingTab(this.app, this));
}
async loadSettings() {
try {
const data = (await this.loadData());
if (data) {
utils_1.Logger.debug('Loading saved settings', 'settings');
this.settings = Object.assign({}, this.settings, data);
}
}
catch (error) {
error_handler_1.ErrorHandler.handleError(error, 'settings load');
}
}
async saveSettings() {
try {
// Validate settings before saving
const validationErrors = (0, utils_1.validatePluginSettings)(this.settings);
if (validationErrors.length > 0) {
utils_1.Logger.error('Validation errors prevented saving settings: ' + validationErrors.join('; '), 'settings');
new obsidian_1.Notice(`Cannot save settings: ${validationErrors[0]}`);
return false;
}
utils_1.Logger.debug('Saving settings: ' + JSON.stringify(this.settings), 'settings');
await this.saveData(this.settings);
utils_1.Logger.info('Settings saved successfully', 'settings');
return true;
}
catch (error) {
error_handler_1.ErrorHandler.handleError(error, 'settings save');
return false;
}
}
// Notify all open ChatView instances when settings change
notifyChatViews() {
const leaves = this.app.workspace.getLeavesOfType('ollama-chat-view');
leaves.forEach((leaf) => {
const view = leaf.view;
if (view instanceof chat_view_1.ChatView) {
view.onSettingsChange(this.settings);
}
});
}
}
exports.default = OllamaPlugin;
class OllamaSettingTab extends obsidian_1.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
// Clear any existing content first to prevent duplicates
this.containerEl.empty();
// Create container for settings
const container = this.containerEl.createDiv();
container.empty();
new obsidian_1.Setting(container)
.setName('Ollama URL')
.setDesc('URL of your Ollama instance')
.addText((text) => text.setValue(this.plugin.settings.ollamaUrl).onChange(async (value) => {
const urlValidation = (0, utils_1.validateOllamaUrl)(value);
if (urlValidation.valid) {
utils_1.Logger.debug('URL changed to: ' + value, 'settings');
this.plugin.settings.ollamaUrl = value;
await this.plugin.saveSettings();
this.plugin.notifyChatViews();
}
else {
utils_1.Logger.warn('Invalid URL format: ' + urlValidation.error, 'settings');
new obsidian_1.Notice(urlValidation.error || 'Invalid Ollama URL format.');
}
}));
new obsidian_1.Setting(container)
.setName('Model')
.setDesc('Model to use for chat')
.addText((text) => text.setValue(this.plugin.settings.model).onChange(async (value) => {
const modelValidation = (0, utils_1.validateModelName)(value);
if (modelValidation.valid) {
utils_1.Logger.debug('Model changed to: ' + value, 'settings');
this.plugin.settings.model = value;
await this.plugin.saveSettings();
this.plugin.notifyChatViews();
}
else {
utils_1.Logger.warn('Invalid model name format: ' + modelValidation.error, 'settings');
new obsidian_1.Notice(modelValidation.error || 'Invalid model name format.');
}
}));
}
hide() {
// Clear the container to prevent duplicate elements
this.containerEl.empty();
}
}
-174
View File
@@ -1,174 +0,0 @@
"use strict";
// src/ollama-client.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.OllamaClient = void 0;
const types_1 = require("./types");
const utils_1 = require("./utils");
class OllamaClient {
constructor(baseURL, model, fetchFn) {
this.abortController = null;
this.maxRetries = 3;
this.baseURL = baseURL;
this.model = model;
this.fetchFn = fetchFn ?? fetch;
}
cancelStream() {
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
}
async *streamChat(messages, tools = []) {
yield* this.streamChatWithRetry(messages, tools, 0);
}
async streamChatAsPromise(messages, tools = []) {
const chunks = [];
for await (const chunk of this.streamChat(messages, tools)) {
chunks.push(chunk);
}
return chunks;
}
async *streamChatWithRetry(messages, tools = [], attempt = 0) {
this.abortController = 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: true,
}),
signal: this.abortController.signal,
});
if (!response.ok) {
if (response.status >= 500 && attempt < this.maxRetries) {
const retryDelay = Math.pow(2, attempt) * 100;
utils_1.Logger.warn(`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`, 'ollama-client');
await new Promise((resolve) => setTimeout(resolve, retryDelay));
yield* this.streamChatWithRetry(messages, tools, attempt + 1);
return;
}
throw new types_1.ApiError(`Ollama API error: ${response.status}`, response.status);
}
if (!response.body) {
throw new Error('No response body');
}
const contentType = response.headers.get('content-type');
if (!contentType || (!contentType.includes('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);
this.throwIfOllamaError(parsed);
const message = this.toOllamaMessage(parsed.message);
if (!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');
}
utils_1.Logger.warn(`Skipped malformed chunk: ${line.substring(0, 80)}... - ${error.message}`, 'ollama-client');
}
}
}
if (buffer.trim()) {
try {
const parsed = JSON.parse(buffer);
this.throwIfOllamaError(parsed);
const message = this.toOllamaMessage(parsed.message);
if (message) {
yield message;
}
}
catch (error) {
if (error instanceof Error && error.message.startsWith('Ollama error:')) {
throw error;
}
utils_1.Logger.warn(`Failed to parse final chunk: ${buffer.substring(0, 80)}...`, 'ollama-client');
}
}
}
finally {
reader.releaseLock();
}
}
finally {
this.abortController = null;
}
}
async chat(messages, tools = []) {
return this.chatWithRetry(messages, tools, 0);
}
async chatWithRetry(messages, tools = [], attempt = 0) {
const controller = new AbortController();
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;
utils_1.Logger.warn(`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`, 'ollama-client');
await new Promise((resolve) => setTimeout(resolve, retryDelay));
return this.chatWithRetry(messages, tools, attempt + 1);
}
throw new types_1.ApiError(`Ollama API error: ${response.status}`, response.status);
}
const data = (await response.json());
return this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] };
}
throwIfOllamaError(parsed) {
if (parsed.error) {
throw new Error(`Ollama error: ${String(parsed.error)}`);
}
}
toOllamaMessage(value) {
if (!value || typeof value !== 'object') {
return null;
}
const record = value;
return {
role: record.role ?? 'assistant',
content: typeof record.content === 'string' ? record.content : '',
tool_calls: record.tool_calls ?? [],
};
}
}
exports.OllamaClient = OllamaClient;
-63
View File
@@ -1,63 +0,0 @@
"use strict";
/**
* Test cases for model name validation
* Tests common Ollama model naming patterns
*/
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("./utils");
const testCases = [
// Valid Ollama model names with colons
{ model: 'llama3:8b', expected: true, description: 'Standard model with colon tag' },
{ model: 'qwen2.5-coder:7b', expected: true, description: 'Model with dot, dash, and colon' },
{ model: 'mistral:latest', expected: true, description: 'Model with latest tag' },
{ model: 'llama3:latest', expected: true, description: 'Another latest tag example' },
{ model: 'llama3:30b', expected: true, description: '30b variant' },
{ model: 'llama3:70b', expected: true, description: '70b variant' },
{ model: 'codellama:7b', expected: true, description: 'Code-specific model' },
{ model: 'llava:7b', expected: true, description: 'Multimodal model' },
{ model: 'granite:7b', expected: true, description: 'Granite model family' },
// Valid model names without colons (should still work)
{ model: 'llama3', expected: true, description: 'Model without tag' },
{ model: 'qwen-2.5', expected: true, description: 'Model with dots and dashes' },
{ model: 'mistral_7b', expected: true, description: 'Model with underscore' },
// Invalid model names
{ model: '', expected: false, description: 'Empty string' },
{ model: 'a', expected: false, description: 'Too short' },
{ model: 'llama@3', expected: false, description: 'Contains invalid @ character' },
{ model: 'llama/3', expected: false, description: 'Contains invalid / character' },
{ model: 'llama 3', expected: false, description: 'Contains spaces' },
{ model: 'llama\t3', expected: false, description: 'Contains tabs' },
{ model: 'llama\n3', expected: false, description: 'Contains newlines' },
{ model: 'very-long-model-name-that-exceeds-the-maximum-length-of-100-characters-and-should-be-rejected-by-validation', expected: false, description: 'Too long' },
// Edge cases
{ model: '123', expected: true, description: 'Numeric model name' },
{ model: '123:456', expected: true, description: 'Numeric with colon' },
{ model: 'a.b-c_d:e', expected: true, description: 'All allowed characters' },
];
function runTests() {
console.log('Running model name validation tests...');
let passed = 0;
let failed = 0;
testCases.forEach(({ model, expected, description }) => {
const result = (0, utils_1.validateModelName)(model);
const success = result.valid === expected;
if (success) {
console.log(`✓ PASS: ${description} (${JSON.stringify(model)})`);
passed++;
}
else {
console.log(`✗ FAIL: ${description} (${JSON.stringify(model)})`);
console.log(` Expected valid: ${expected}, Got valid: ${result.valid}`);
if (result.error) {
console.log(` Error: ${result.error}`);
}
failed++;
}
});
console.log(`\nTest Results: ${passed} passed, ${failed} failed`);
return failed === 0;
}
if (require.main === module) {
const success = runTests();
process.exit(success ? 0 : 1);
}
-114
View File
@@ -1,114 +0,0 @@
"use strict";
// src/tool-executor.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.ToolExecutor = void 0;
const utils_1 = require("./utils");
// Disallow characters that are invalid in file paths
const INVALID_PATH_CHARS = /[<>:"|?*~]/;
const MAX_PATH_LENGTH = 200;
const FORBIDDEN_DIRS = ['.obsidian', '.git'];
class ToolExecutor {
constructor(vault, app) {
this.vault = vault;
this.app = app;
}
isSafePath(path) {
// Reject empty paths
if (!path || path.trim().length === 0) {
return false;
}
// Reject paths that are too long
if (path.length > MAX_PATH_LENGTH) {
return false;
}
// Reject paths with invalid characters
if (INVALID_PATH_CHARS.test(path)) {
return false;
}
// Reject absolute paths
if (path.startsWith('/') || path.startsWith('\\')) {
return false;
}
// Reject Windows drive letters (e.g., C:)
if (/^[a-zA-Z]:/.test(path)) {
return false;
}
// Reject paths containing backslashes (Windows-style path separators)
if (path.includes('\\')) {
return false;
}
// Reject paths that traverse to parent directories
const normalized = path.replace(/^(\.\/)+/, '');
if (normalized.split('/').includes('..')) {
return false;
}
// Reject forbidden directories
for (const dir of FORBIDDEN_DIRS) {
if (normalized.startsWith(`${dir}/`) || normalized.startsWith(`${dir}\\`)) {
return false;
}
if (normalized.includes(`/${dir}/`) || normalized.includes(`\\${dir}\\`)) {
return false;
}
}
return true;
}
async handleToolCall(toolCall) {
try {
const toolName = toolCall.function?.name;
const rawArgs = toolCall.function?.arguments;
if (!toolName) {
throw new Error('Tool name is required');
}
// Parse arguments whether they're a string or object
let parsedArgs;
if (typeof rawArgs === 'string') {
try {
parsedArgs = (0, utils_1.safeParseJson)(rawArgs);
}
catch {
throw new Error('Invalid JSON arguments');
}
}
else if (rawArgs && typeof rawArgs === 'object') {
parsedArgs = rawArgs;
}
else {
throw new Error('Arguments must be an object or JSON string');
}
// Process the tool call based on its type
switch (toolName) {
case 'create_file':
return await this.handleCreateFile(parsedArgs);
default:
return { success: false, message: `Unknown tool: ${toolName}` };
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(errorMessage);
}
}
async handleCreateFile(args) {
const path = args.path;
const content = args.content;
if (typeof path !== 'string') {
throw new Error('Path must be a string');
}
if (typeof content !== 'string') {
throw new Error('Content must be a string');
}
if (!this.isSafePath(path)) {
throw new Error('Invalid file path detected');
}
try {
await this.vault.create(path, content);
return { success: true, message: 'File created successfully' };
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(errorMessage);
}
}
}
exports.ToolExecutor = ToolExecutor;
-79
View File
@@ -1,79 +0,0 @@
"use strict";
// src/types.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_SETTINGS = exports.PathValidationError = exports.ToolExecutionError = exports.StreamingError = exports.ValidationError = exports.ApiError = exports.NetworkError = exports.OllamaError = exports.ErrorType = void 0;
// ============================================================
// Error Type Hierarchy
// ============================================================
var ErrorType;
(function (ErrorType) {
ErrorType["NETWORK_ERROR"] = "network_error";
ErrorType["API_ERROR"] = "api_error";
ErrorType["VALIDATION_ERROR"] = "validation_error";
ErrorType["STREAMING_ERROR"] = "streaming_error";
ErrorType["TOOL_EXECUTION_ERROR"] = "tool_execution_error";
ErrorType["PATH_VALIDATION_ERROR"] = "path_validation_error";
ErrorType["UNKNOWN_ERROR"] = "unknown_error";
})(ErrorType || (exports.ErrorType = ErrorType = {}));
class OllamaError extends Error {
constructor(message, type) {
super(message);
this.type = type;
Object.setPrototypeOf(this, OllamaError.prototype);
}
}
exports.OllamaError = OllamaError;
class NetworkError extends OllamaError {
constructor(message, statusCode) {
super(message, ErrorType.NETWORK_ERROR);
this.statusCode = statusCode;
Object.setPrototypeOf(this, NetworkError.prototype);
}
}
exports.NetworkError = NetworkError;
class ApiError extends OllamaError {
constructor(message, statusCode) {
super(message, ErrorType.API_ERROR);
this.statusCode = statusCode;
Object.setPrototypeOf(this, ApiError.prototype);
}
}
exports.ApiError = ApiError;
class ValidationError extends OllamaError {
constructor(message, details) {
super(message, ErrorType.VALIDATION_ERROR);
this.details = details;
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
exports.ValidationError = ValidationError;
class StreamingError extends OllamaError {
constructor(message) {
super(message, ErrorType.STREAMING_ERROR);
Object.setPrototypeOf(this, StreamingError.prototype);
}
}
exports.StreamingError = StreamingError;
class ToolExecutionError extends OllamaError {
constructor(message, toolName) {
super(message, ErrorType.TOOL_EXECUTION_ERROR);
this.toolName = toolName;
Object.setPrototypeOf(this, ToolExecutionError.prototype);
}
}
exports.ToolExecutionError = ToolExecutionError;
class PathValidationError extends OllamaError {
constructor(message, path) {
super(message, ErrorType.PATH_VALIDATION_ERROR);
this.path = path;
Object.setPrototypeOf(this, PathValidationError.prototype);
}
}
exports.PathValidationError = PathValidationError;
exports.DEFAULT_SETTINGS = {
ollamaUrl: 'http://localhost:11434',
model: 'llama3',
vaultSearchLimit: 3,
maxMessageHistory: 50,
lastIndexTime: 0,
};
-180
View File
@@ -1,180 +0,0 @@
"use strict";
// src/utils.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.Logger = void 0;
exports.validateOllamaUrl = validateOllamaUrl;
exports.validateModelName = validateModelName;
exports.validatePluginSettings = validatePluginSettings;
exports.safeParseJson = safeParseJson;
exports.sanitizeFilePath = sanitizeFilePath;
exports.isValidHttpUrl = isValidHttpUrl;
exports.convertMarkdownToHtml = convertMarkdownToHtml;
// ==================== Logger ====================
var LogLevel;
(function (LogLevel) {
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
LogLevel[LogLevel["INFO"] = 1] = "INFO";
LogLevel[LogLevel["WARN"] = 2] = "WARN";
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
})(LogLevel || (LogLevel = {}));
const SEVERITY_ORDER = {
debug: LogLevel.DEBUG,
info: LogLevel.INFO,
warn: LogLevel.WARN,
error: LogLevel.ERROR,
};
class Logger {
static setLevel(level) {
if (typeof level === 'string') {
const lowerLevel = level.toLowerCase();
Logger.minLevel = SEVERITY_ORDER[lowerLevel] ?? LogLevel.DEBUG;
}
else {
Logger.minLevel = level;
}
}
static debug(message, category = 'general') {
if (LogLevel.DEBUG >= Logger.minLevel) {
console.debug(`[${category}] DEBUG: ${message}`);
}
}
static info(message, category = 'general') {
if (LogLevel.INFO >= Logger.minLevel) {
console.info(`[${category}] INFO: ${message}`);
}
}
static warn(message, category = 'general') {
if (LogLevel.WARN >= Logger.minLevel) {
console.warn(`[${category}] WARN: ${message}`);
}
}
static error(message, category = 'general') {
if (LogLevel.ERROR >= Logger.minLevel) {
console.error(`[${category}] ERROR: ${message}`);
}
}
}
exports.Logger = Logger;
Logger.minLevel = LogLevel.DEBUG;
// ==================== URL & Model Validation ====================
function validateOllamaUrl(url) {
if (typeof url !== 'string' || !url.trim()) {
return { valid: false, error: 'URL cannot be empty' };
}
const trimmedUrl = url.trim();
if (trimmedUrl.endsWith('/')) {
return { valid: false, error: 'URL should not end with a slash' };
}
try {
const parsed = new URL(trimmedUrl);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' };
}
return { valid: true };
}
catch {
return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' };
}
}
function validateModelName(model) {
if (typeof model !== 'string') {
return { valid: false, error: 'Model name must be a string' };
}
const trimmedModel = model.trim();
// Explicit check for empty string after trimming
if (!trimmedModel || trimmedModel.length === 0) {
return { valid: false, error: 'Model name cannot be empty' };
}
if (trimmedModel.length < 2) {
return { valid: false, error: 'Model name must be at least 2 characters long' };
}
if (trimmedModel.length > 100) {
return { valid: false, error: 'Model name must be less than 100 characters long' };
}
if (!/^[a-zA-Z0-9._:-]+$/.test(trimmedModel)) {
return {
valid: false,
error: 'Model name can only contain letters, numbers, dots, dashes, underscores, and colons',
};
}
return { valid: true };
}
function validatePluginSettings(settings) {
const errors = [];
const urlValidation = validateOllamaUrl(settings.ollamaUrl);
if (!urlValidation.valid) {
errors.push(`Invalid Ollama URL: ${urlValidation.error}`);
}
const modelValidation = validateModelName(settings.model);
if (!modelValidation.valid) {
errors.push(`Invalid Model Name: ${modelValidation.error}`);
}
return errors;
}
// ==================== Safe JSON Parsing ====================
const MAX_JSON_SIZE = 1000000;
const MAX_JSON_NESTING = 24;
function countNestingDepth(value, depth = 0) {
if (depth > MAX_JSON_NESTING) {
return depth;
}
if (Array.isArray(value)) {
return Math.max(...value.map((item) => countNestingDepth(item, depth + 1)), depth);
}
if (value !== null && typeof value === 'object') {
const entries = Object.values(value);
if (entries.length === 0)
return depth;
return Math.max(...entries.map((item) => countNestingDepth(item, depth + 1)), depth);
}
return depth;
}
function safeParseJson(jsonString) {
if (typeof jsonString !== 'string') {
throw new Error('Input must be a string');
}
if (jsonString.length > MAX_JSON_SIZE) {
throw new Error('JSON input too large');
}
let parsed;
try {
parsed = JSON.parse(jsonString);
}
catch {
throw new Error('Invalid JSON');
}
// Check for dangerous prototype pollution patterns
const reStringified = JSON.stringify(parsed);
if (reStringified.includes('constructor') ||
reStringified.includes('prototype') ||
reStringified.includes('__proto__') ||
reStringified.includes('function')) {
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;
}
// ==================== Path & File Utilities ====================
function sanitizeFilePath(path) {
if (path.includes('..')) {
throw new Error('Invalid path - cannot contain .. segments');
}
return path;
}
// ==================== HTTP Helpers ====================
function isValidHttpUrl(url) {
try {
const parsed = new URL(url);
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
}
catch {
return false;
}
}
// ==================== Markdown Utilities ====================
function convertMarkdownToHtml(markdown) {
return markdown.replace(/\n/g, '<br>').replace(/# (.+)/g, '<h1>$1</h1>');
}
-208
View File
@@ -1,208 +0,0 @@
"use strict";
// src/vault-indexer.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.VaultIndexer = void 0;
const utils_1 = require("./utils");
class VaultIndexer {
constructor(vault) {
this.vault = null;
this.vault = vault;
}
async searchVault(query, limit = 5) {
if (!query || !query.trim()) {
return [];
}
if (!this.vault) {
throw new Error('Vault-like object not provided to VaultIndexer');
}
const queryTokens = this.tokenize(query.trim());
const vault = this.vault;
const allFiles = vault.getMarkdownFiles();
const results = await this.processFilesInBatches(vault, allFiles, queryTokens);
return results
.filter((result) => result !== null)
.sort((a, b) => b.score - a.score)
.slice(0, limit);
}
async processFilesInBatches(vault, files, queryTokens) {
const batchSize = 10;
const results = [];
const seenPaths = new Set();
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
const batchResults = 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 = {
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) {
utils_1.Logger.warn(`Failed to process ${file.path}: ${error instanceof Error ? error.message : String(error)}`, 'vault-indexer');
return null;
}
}));
const validResults = batchResults.filter((result) => result !== null);
results.push(...validResults);
if (results.length >= 50) {
break;
}
}
return results;
}
tokenize(text) {
const stopWords = new Set([
'the',
'a',
'an',
'and',
'or',
'but',
'is',
'are',
'was',
'were',
'in',
'on',
'at',
'to',
'of',
'for',
'with',
'as',
'by',
'it',
'its',
'that',
'this',
'these',
'those',
]);
return text
.toLowerCase()
.split(/\W+/)
.filter((token) => token.length > 1 && !stopWords.has(token));
}
tokenizeContent(content) {
const tokens = [];
const headings = [];
const frontmatter = {};
let firstParagraph;
const frontmatterMatch = content.match(/^---(.*?)---/s);
if (frontmatterMatch) {
try {
const frontmatterContent = frontmatterMatch[1];
const lines = frontmatterContent.trim().split('\n');
for (const line of lines) {
const [key, ...valueParts] = line.split(':');
if (!key)
continue;
const value = valueParts.join(':').trim();
if (key.trim() === 'title') {
if (value) {
frontmatter.title = value;
}
}
else if (key.trim() === 'tags') {
if (value) {
frontmatter.tags = value;
}
}
}
}
catch {
utils_1.Logger.warn('Failed to parse frontmatter', 'vault-indexer');
}
}
const headingMatches = content.match(/^#{1,6} (.*?)$/gm);
if (headingMatches) {
headings.push(...headingMatches.map((h) => 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 };
}
calculateWeightedScore(tokenized, queryTokens, file) {
let totalScore = 0;
const matchedTokens = new Set();
for (const queryToken of queryTokens) {
let tokenScore = 0;
const stemmed = this.stemToken(queryToken);
let matched = false;
if (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) => 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),
};
}
stemToken(token) {
// Improved stemmer that handles edge cases
if (token.length <= 3)
return token; // Don't stem very short tokens
if (token.endsWith('s'))
return token.slice(0, -1);
if (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;
}
exactMatch(content, token) {
const stemmedToken = this.stemToken(token);
return content.toLowerCase().includes(stemmedToken);
}
}
exports.VaultIndexer = VaultIndexer;