All files tool-executor.ts

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

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132        2x     2x 2x 2x   2x         63x 63x         23x 1x       22x 1x       21x 8x       13x 3x       10x         10x 1x       9x 9x 1x       8x 16x     16x         8x       28x 28x 28x   28x           28x 27x 27x   1x   1x 1x           27x   26x   1x     20x 20x         26x 26x   26x 2x     24x 1x     23x 15x     8x 8x 7x   1x 1x        
// src/tool-executor.ts
 
import { Vault, App } from 'obsidian';
import type { ToolCall, ToolResult } from './types';
import { safeParseJson } from './utils';
 
// Disallow characters that are invalid in file paths
const INVALID_PATH_CHARS = /[<>:"|?*~]/;
const MAX_PATH_LENGTH = 200;
const FORBIDDEN_DIRS = ['.obsidian', '.git'];
 
export class ToolExecutor {
  private vault: Vault;
  private app: App;
 
  constructor(vault: Vault, app: App) {
    this.vault = vault;
    this.app = app;
  }
 
  private isSafePath(path: string): boolean {
    // Reject empty paths
    if (!path || path.trim().length === 0) {
      return false;
    }
 
    // Reject paths that are too long
    if (path.length > MAX_PATH_LENGTH) {
      return false;
    }
 
    // Reject paths with invalid characters
    if (INVALID_PATH_CHARS.test(path)) {
      return false;
    }
 
    // Reject absolute paths
    if (path.startsWith('/') || path.startsWith('\\')) {
      return false;
    }
 
    // Reject Windows drive letters (e.g., C:)
    Iif (/^[a-zA-Z]:/.test(path)) {
      return false;
    }
 
    // Reject paths containing backslashes (Windows-style path separators)
    if (path.includes('\\')) {
      return false;
    }
 
    // Reject paths that traverse to parent directories
    const normalized = path.replace(/^(\.\/)+/, '');
    if (normalized.split('/').includes('..')) {
      return false;
    }
 
    // Reject forbidden directories
    for (const dir of FORBIDDEN_DIRS) {
      Iif (normalized.startsWith(`${dir}/`) || normalized.startsWith(`${dir}\\`)) {
        return false;
      }
      Iif (normalized.includes(`/${dir}/`) || normalized.includes(`\\${dir}\\`)) {
        return false;
      }
    }
 
    return true;
  }
 
  async handleToolCall(toolCall: ToolCall): Promise<ToolResult> {
    try {
      const toolName = toolCall.function?.name;
      const rawArgs = toolCall.function?.arguments;
 
      Iif (!toolName) {
        throw new Error('Tool name is required');
      }
 
      // Parse arguments whether they're a string or object
      let parsedArgs: Record<string, unknown>;
      if (typeof rawArgs === 'string') {
        try {
          parsedArgs = safeParseJson(rawArgs) as Record<string, unknown>;
        } catch {
          throw new Error('Invalid JSON arguments');
        }
      } else if (rawArgs && typeof rawArgs === 'object') {
        parsedArgs = rawArgs;
      } else E{
        throw new Error('Arguments must be an object or JSON string');
      }
 
      // Process the tool call based on its type
      switch (toolName) {
        case 'create_file':
          return await this.handleCreateFile(parsedArgs);
        default:
          return { success: false, message: `Unknown tool: ${toolName}` };
      }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      throw new Error(errorMessage);
    }
  }
 
  private async handleCreateFile(args: Record<string, unknown>): Promise<ToolResult> {
    const path = args.path;
    const content = args.content;
 
    if (typeof path !== 'string') {
      throw new Error('Path must be a string');
    }
 
    if (typeof content !== 'string') {
      throw new Error('Content must be a string');
    }
 
    if (!this.isSafePath(path)) {
      throw new Error('Invalid file path detected');
    }
 
    try {
      await this.vault.create(path, content);
      return { success: true, message: 'File created successfully' };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      throw new Error(errorMessage);
    }
  }
}