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        1x     1x 1x 1x   1x         27x 27x         22x 1x       21x 1x       20x 8x       12x 3x       9x         9x 1x       8x 8x 1x       7x 14x     14x         7x       27x 27x 27x   27x           27x 26x 26x   1x   1x 1x           26x   25x   1x     20x 20x         25x 25x   25x 2x     23x 1x     22x 15x     7x 7x 6x   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.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 as Record<string, unknown>;
      } 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);
    }
  }
}