Remove deprecated files and update coverage reports
This commit is contained in:
Executable → Regular
Executable → Regular
Executable → Regular
+448
-448
File diff suppressed because it is too large
Load Diff
Executable → Regular
+122
-122
@@ -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"> </span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">10x</span>
|
||||
<span class="cline-any cline-yes">10x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">10x</span>
|
||||
<span class="cline-any cline-yes">9x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">16x</span>
|
||||
<span class="cline-any cline-yes">7x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">9x</span>
|
||||
<span class="cline-any cline-yes">8x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">7x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -264,73 +264,73 @@
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">14x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">14x</span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">12x</span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">9x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">8x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">7x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">6x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">5x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">// src/error-handler.ts
|
||||
@@ -348,124 +348,124 @@ import {
|
||||
} from './types';
|
||||
|
||||
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>
|
||||
|
||||
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>
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
return 'An unexpected error occurred';
|
||||
<span class="cstat-no" title="statement not covered" > return 'An unexpected error occurred';</span>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
}
|
||||
|
||||
private static getUserFriendlyValidationMessage(error: OllamaError): string {
|
||||
if (error instanceof ValidationError && error.details?.field) {
|
||||
const fieldMsg = error.details.field.charAt(0).toUpperCase() + error.details.field.slice(1);
|
||||
return `Invalid ${fieldMsg.toLowerCase()}. ${error.details.message ?? <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 && 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>
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
// 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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
return 'An unexpected error occurred';
|
||||
<span class="cstat-no" title="statement not covered" > return 'An unexpected error occurred';</span>
|
||||
}
|
||||
|
||||
// -- Factory methods --
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
}
|
||||
</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
|
Before Width: | Height: | Size: 445 B After Width: | Height: | Size: 445 B |
Executable → Regular
+83
-83
@@ -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>
|
||||
|
||||
Executable → Regular
+279
-279
@@ -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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">48x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">48x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">14x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">14x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">14x</span>
|
||||
<span class="cline-any cline-yes">14x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">14x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">9x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </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"> </span>
|
||||
<span class="cline-any cline-yes">7x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">7x</span>
|
||||
<span class="cline-any cline-yes">7x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">7x</span>
|
||||
<span class="cline-any cline-yes">61x</span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-yes">8x</span>
|
||||
<span class="cline-any cline-yes">8x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">8x</span>
|
||||
<span class="cline-any cline-yes">8x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">53x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">51x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">5x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">7x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">14x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">5x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">8x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">8x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">4x</span>
|
||||
<span class="cline-any cline-yes">4x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">9x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">12x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">11x</span>
|
||||
<span class="cline-any cline-yes">11x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -544,40 +544,40 @@ export class OllamaClient {
|
||||
this.fetchFn = fetchFn ?? fetch;
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
}
|
||||
|
||||
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<OllamaMessage, void, unknown> {
|
||||
yield* this.streamChatWithRetry(messages, tools, 0);
|
||||
<span class="cstat-no" title="statement not covered" > yield* this.streamChatWithRetry(messages, tools, 0);</span>
|
||||
}
|
||||
|
||||
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<OllamaMessage[]> {
|
||||
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>
|
||||
}
|
||||
|
||||
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<OllamaMessage, void, unknown> {
|
||||
this.abortController = new AbortController();
|
||||
<span class="cstat-no" title="statement not covered" > this.abortController = new AbortController();</span>
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
<span class="missing-if-branch" title="if path not taken" >I</span>if (response.status >= 500 && <span class="branch-1 cbranch-no" title="branch not covered" >attempt < 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 >= 500 && attempt < 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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || (!contentType.includes('ndjson') && !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') && !contentType.includes('json'))) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > throw new Error('Invalid response format');</span>
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
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>
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
<span class="cstat-no" title="statement not covered" > buffer += decoder.decode(value, { stream: true });</span>
|
||||
|
||||
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>
|
||||
|
||||
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>
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(line) as Record<string, unknown>;
|
||||
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<string, unknown>;</span>
|
||||
<span class="cstat-no" title="statement not covered" > this.throwIfOllamaError(parsed);</span>
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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 && 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 && error.message.startsWith('Ollama error:')) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > throw error;</span>
|
||||
}
|
||||
|
||||
malformedCount++;
|
||||
if (malformedCount > 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 > maxMalformed) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > throw new Error('Too many malformed chunks in stream');</span>
|
||||
}
|
||||
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
<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<string, unknown>;</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>
|
||||
}
|
||||
}
|
||||
|
||||
async chat(messages: OllamaMessage[], tools: OllamaTool[] = <span class="branch-0 cbranch-no" title="branch not covered" >[])</span>: Promise<OllamaMessage> {
|
||||
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<OllamaMessage> {
|
||||
<span class="cstat-no" title="statement not covered" > return this.chatWithRetry(messages, tools, 0);</span>
|
||||
}
|
||||
|
||||
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<OllamaMessage> {
|
||||
const controller = new AbortController();
|
||||
const controller = <span class="cstat-no" title="statement not covered" >new AbortController();</span>
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status >= 500 && attempt < 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 >= 500 && attempt < 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) => 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) => <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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
private throwIfOllamaError(parsed: Record<string, unknown>): 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<string, unknown>): 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>
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
const record = value as Partial<OllamaMessage>;
|
||||
return {
|
||||
const record = <span class="cstat-no" title="statement not covered" >value as Partial<OllamaMessage>;</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>
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
|
Before Width: | Height: | Size: 138 B After Width: | Height: | Size: 138 B |
Executable → Regular
Executable → Regular
+108
-108
@@ -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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">58x</span>
|
||||
<span class="cline-any cline-yes">58x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">21x</span>
|
||||
<span class="cline-any cline-yes">8x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">13x</span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">10x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">10x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">8x</span>
|
||||
<span class="cline-any cline-yes">16x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">16x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">8x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-yes">28x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">27x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">26x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">20x</span>
|
||||
<span class="cline-any cline-yes">20x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">26x</span>
|
||||
<span class="cline-any cline-yes">26x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">26x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">24x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">23x</span>
|
||||
<span class="cline-any cline-yes">15x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </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"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -347,23 +347,23 @@ export class ToolExecutor {
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
// Reject paths that are too long
|
||||
if (path.length > MAX_PATH_LENGTH) {
|
||||
return false;
|
||||
<span class="missing-if-branch" title="if path not taken" >I</span>if (path.length > MAX_PATH_LENGTH) {
|
||||
<span class="cstat-no" title="statement not covered" > return false;</span>
|
||||
}
|
||||
|
||||
// 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>
|
||||
}
|
||||
|
||||
// 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>
|
||||
}
|
||||
|
||||
// Reject Windows drive letters (e.g., C:)
|
||||
@@ -372,14 +372,14 @@ export class ToolExecutor {
|
||||
}
|
||||
|
||||
// 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>
|
||||
}
|
||||
|
||||
// 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>
|
||||
}
|
||||
|
||||
// Reject forbidden directories
|
||||
@@ -410,11 +410,11 @@ export class ToolExecutor {
|
||||
try {
|
||||
parsedArgs = safeParseJson(rawArgs) as Record<string, unknown>;
|
||||
} 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 && 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 && 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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,24 +435,24 @@ export class ToolExecutor {
|
||||
const path = args.path;
|
||||
const content = args.content;
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
Executable → Regular
+55
-55
@@ -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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -279,9 +279,9 @@
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -289,9 +289,9 @@
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -304,16 +304,16 @@
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -321,9 +321,9 @@
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -331,9 +331,9 @@
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -454,30 +454,30 @@ export enum ErrorType {
|
||||
export class OllamaError extends Error {
|
||||
public readonly type: ErrorType;
|
||||
|
||||
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>
|
||||
}
|
||||
}
|
||||
|
||||
export class NetworkError extends OllamaError {
|
||||
public readonly statusCode?: number;
|
||||
|
||||
constructor(message: string, statusCode?: number) {
|
||||
super(message, ErrorType.NETWORK_ERROR);
|
||||
this.statusCode = statusCode;
|
||||
Object.setPrototypeOf(this, NetworkError.prototype);
|
||||
<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>
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiError extends OllamaError {
|
||||
public readonly statusCode?: number;
|
||||
|
||||
constructor(message: string, statusCode?: number) {
|
||||
super(message, ErrorType.API_ERROR);
|
||||
this.statusCode = statusCode;
|
||||
Object.setPrototypeOf(this, ApiError.prototype);
|
||||
<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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,37 +489,37 @@ export interface ValidationFieldDetails {
|
||||
export class ValidationError extends OllamaError {
|
||||
public readonly details?: ValidationFieldDetails;
|
||||
|
||||
constructor(message: string, details?: ValidationFieldDetails) {
|
||||
super(message, ErrorType.VALIDATION_ERROR);
|
||||
this.details = details;
|
||||
Object.setPrototypeOf(this, ValidationError.prototype);
|
||||
<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>
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
}
|
||||
|
||||
export class ToolExecutionError extends OllamaError {
|
||||
public readonly toolName: string;
|
||||
|
||||
constructor(message: string, toolName: string) {
|
||||
super(message, ErrorType.TOOL_EXECUTION_ERROR);
|
||||
this.toolName = toolName;
|
||||
Object.setPrototypeOf(this, ToolExecutionError.prototype);
|
||||
<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>
|
||||
}
|
||||
}
|
||||
|
||||
export class PathValidationError extends OllamaError {
|
||||
public readonly path: string;
|
||||
|
||||
constructor(message: string, path: string) {
|
||||
super(message, ErrorType.PATH_VALIDATION_ERROR);
|
||||
this.path = path;
|
||||
Object.setPrototypeOf(this, PathValidationError.prototype);
|
||||
<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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Executable → Regular
+164
-164
@@ -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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">55x</span>
|
||||
<span class="cline-any cline-yes">55x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -326,126 +326,126 @@
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">10x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">10x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">6x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">5x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">5x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">5x</span>
|
||||
<span class="cline-any cline-yes">5x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">14x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">14x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">13x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">12x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">11x</span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">8x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">5x</span>
|
||||
<span class="cline-any cline-yes">4x</span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">4x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">5x</span>
|
||||
<span class="cline-any cline-yes">5x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">109x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">108x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-yes">54x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">37x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">5x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">34x</span>
|
||||
<span class="cline-any cline-yes">34x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">4x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">29x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -460,11 +460,11 @@
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -515,9 +515,9 @@ export class Logger {
|
||||
}
|
||||
}
|
||||
|
||||
static warn(message: string, category: string = <span class="branch-0 cbranch-no" title="branch not covered" >'general')</span>: void {
|
||||
if (LogLevel.WARN >= 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 >= Logger.minLevel) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > console.warn(`[${category}] WARN: ${message}`);</span>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,72 +530,72 @@ export class Logger {
|
||||
|
||||
// ==================== URL & Model Validation ====================
|
||||
|
||||
export function validateOllamaUrl(url: string): { valid: boolean; error?: string } {
|
||||
if (typeof url !== 'string' || !url.trim()) {
|
||||
return { valid: false, error: 'URL cannot be empty' };
|
||||
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>
|
||||
}
|
||||
|
||||
const trimmedUrl = url.trim();
|
||||
const trimmedUrl = <span class="cstat-no" title="statement not covered" >url.trim();</span>
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(trimmedUrl);
|
||||
<span class="missing-if-branch" title="if path not taken" >I</span>if (parsed.protocol !== 'http:' && 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:' && 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>
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
const trimmedModel = model.trim();
|
||||
const trimmedModel = <span class="cstat-no" title="statement not covered" >model.trim();</span>
|
||||
|
||||
// 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>
|
||||
}
|
||||
|
||||
if (trimmedModel.length < 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 < 2) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Model name must be at least 2 characters long' };</span>
|
||||
}
|
||||
|
||||
if (trimmedModel.length > 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 > 100) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Model name must be less than 100 characters long' };</span>
|
||||
}
|
||||
|
||||
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',
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
<span class="cstat-no" title="statement not covered" > return { valid: true };</span>
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
return errors;
|
||||
<span class="cstat-no" title="statement not covered" > return errors;</span>
|
||||
}
|
||||
|
||||
// ==================== Safe JSON Parsing ====================
|
||||
@@ -604,50 +604,50 @@ const MAX_JSON_SIZE = 1_000_000;
|
||||
const MAX_JSON_NESTING = 24;
|
||||
|
||||
function countNestingDepth(value: unknown, depth: number = 0): number {
|
||||
if (depth > MAX_JSON_NESTING) {
|
||||
return depth;
|
||||
<span class="missing-if-branch" title="if path not taken" >I</span>if (depth > 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) => <span class="cstat-no" title="statement not covered" >countNestingDepth(item, depth + 1))</span>, depth);</span>
|
||||
}
|
||||
if (value !== null && typeof value === 'object') {
|
||||
const entries = Object.values(value as Record<string, unknown>);
|
||||
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) => countNestingDepth(item, depth + 1)), depth);
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
export function safeParseJson(jsonString: string): unknown {
|
||||
if (typeof jsonString !== 'string') {
|
||||
throw new Error('Input must be a string');
|
||||
<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>
|
||||
}
|
||||
|
||||
if (jsonString.length > 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 > MAX_JSON_SIZE) {
|
||||
<span class="cstat-no" title="statement not covered" > throw new Error('JSON input too large');</span>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
// 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>
|
||||
}
|
||||
|
||||
// Check nesting depth
|
||||
if (countNestingDepth(parsed) > 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) > MAX_JSON_NESTING) {
|
||||
<span class="cstat-no" title="statement not covered" > throw new Error('JSON nesting too deep');</span>
|
||||
}
|
||||
|
||||
return parsed;
|
||||
@@ -664,12 +664,12 @@ export function <span class="fstat-no" title="function not covered" >sanitizeFil
|
||||
|
||||
// ==================== HTTP Helpers ====================
|
||||
|
||||
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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Executable → Regular
+258
-258
@@ -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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">62x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">62x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">28x</span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">26x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </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"> </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"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-yes">15x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">15x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">26x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">95x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">95x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">362x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -514,25 +514,25 @@
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">60x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">60x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">60x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -540,56 +540,56 @@
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">58x</span>
|
||||
<span class="cline-any cline-yes">58x</span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-yes">64x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-yes">64x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">58x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -597,16 +597,16 @@
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </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"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">63x</span>
|
||||
<span class="cline-any cline-yes">63x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -651,8 +651,8 @@ class VaultIndexer {
|
||||
}
|
||||
|
||||
async searchVault(query: string, limit: number = <span class="branch-0 cbranch-no" title="branch not covered" >5)</span>: Promise<VaultIndexEntry[]> {
|
||||
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>
|
||||
}
|
||||
|
||||
<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);
|
||||
|
||||
return results
|
||||
.filter((result): result is NonNullable<typeof result> => result !== null)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.filter(<span class="fstat-no" title="function not covered" >(r</span>esult): result is NonNullable<typeof result> => <span class="cstat-no" title="statement not covered" >result !== null)</span>
|
||||
.sort(<span class="fstat-no" title="function not covered" >(a</span>, b) => <span class="cstat-no" title="statement not covered" >b.score - a.score)</span>
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
@@ -680,43 +680,43 @@ class VaultIndexer {
|
||||
const seenPaths = new Set<string>();
|
||||
|
||||
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: 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) => {
|
||||
<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 > 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>
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const validResults = batchResults.filter(
|
||||
(result): result is NonNullable<typeof result> => 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<typeof result> => <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>
|
||||
|
||||
<span class="missing-if-branch" title="if path not taken" >I</span>if (results.length >= 50) {
|
||||
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (results.length >= 50) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > break;</span>
|
||||
}
|
||||
}
|
||||
@@ -758,28 +758,28 @@ class VaultIndexer {
|
||||
.filter((token) => token.length > 1 && !stopWords.has(token));
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
const headingMatches = content.match(/^#{1,6} (.*?)$/gm);
|
||||
if (headingMatches) {
|
||||
headings.push(...headingMatches.map((h: string) => 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) => <span class="cstat-no" title="statement not covered" >h.replace(/^#{1,6} /, ''))</span>);</span>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
return { tokens, headings, frontmatter, firstParagraph };
|
||||
<span class="cstat-no" title="statement not covered" > return { tokens, headings, frontmatter, firstParagraph };</span>
|
||||
}
|
||||
|
||||
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<string> = new Set<string>();
|
||||
let totalScore = <span class="cstat-no" title="statement not covered" >0;</span>
|
||||
const matchedTokens: Set<string> = <span class="cstat-no" title="statement not covered" >new Set<string>();</span>
|
||||
|
||||
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>
|
||||
|
||||
<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 &&
|
||||
<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 &&
|
||||
file.basename &&
|
||||
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>
|
||||
}
|
||||
|
||||
if (tokenized.frontmatter?.tags && 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 && 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>
|
||||
}
|
||||
|
||||
if (tokenized.headings.some((heading: string) => 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) => <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>
|
||||
}
|
||||
|
||||
if (tokenized.firstParagraph && 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 && 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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
<span class="cstat-no" title="statement not covered" > return {</span>
|
||||
score: totalScore,
|
||||
matchedFields: Array.from(matchedTokens),
|
||||
};
|
||||
}
|
||||
|
||||
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 <= 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') && <span class="branch-1 cbranch-no" title="branch not covered" >token.length > 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') && token.length > 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 <= 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') && token.length > 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') && token.length > 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>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -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-_]+$/;
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
@@ -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>');
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user