andy-stack_vaultkeeper-ai/Services/FileSystemService.ts
Andrew Beal 065d475d74 Refactor file patching from unified diff to direct string replace
This change simplifies the patch mechanism by replacing the unified diff format with a straightforward find-and-replace approach. The PatchVaultFile function now accepts oldContent and newContent parameters instead of a diff patch string, making it more intuitive and reliable for AI agents to use.

Key changes:
- Update PatchVaultFile function signature and documentation
- Replace diff.applyPatch() with String.replace()
- Simplify error handling for content matching
- Update all tests to reflect new API
- Remove unused applyPatch method from DiffService
- Fix diff container height calculation in CSS
- Fix error notification logic in ChatService
- Fix conversation deletion order in TopBar component
2025-12-05 23:02:55 +00:00

106 lines
No EOL
5.5 KiB
TypeScript

import { TAbstractFile, TFile, TFolder } from "obsidian";
import { Resolve } from "./DependencyService";
import { Services } from "./Services";
import type { VaultService } from "./VaultService";
import type { ISearchMatch } from "../Helpers/SearchTypes";
import { Exception } from "Helpers/Exception";
export class FileSystemService {
private readonly vaultService: VaultService;
public constructor() {
this.vaultService = Resolve<VaultService>(Services.VaultService);
}
public async exists(filePath: string, allowAccessToPluginRoot: boolean = false): Promise<boolean> {
return await this.vaultService.exists(filePath, allowAccessToPluginRoot);
}
public async readFile(filePath: string, allowAccessToPluginRoot: boolean = false): Promise<string | Error> {
const file: TAbstractFile | null = this.vaultService.getAbstractFileByPath(filePath, allowAccessToPluginRoot);
if (file && file instanceof TFile) {
return await this.vaultService.read(file, allowAccessToPluginRoot);
}
return Exception.new(`Path is a folder, not a file: ${filePath}`);
}
public async writeFile(filePath: string, content: string, allowAccessToPluginRoot: boolean = false, requiresConfirmation: boolean = true): Promise<TFile | Error> {
const file: TAbstractFile | null = this.vaultService.getAbstractFileByPath(filePath, allowAccessToPluginRoot);
if (file == null || !(file instanceof TFile)) {
return await this.vaultService.create(filePath, content, allowAccessToPluginRoot, requiresConfirmation);
}
return await this.vaultService.modify(file, content, allowAccessToPluginRoot, requiresConfirmation);
}
public async patchFile(filePath: string, oldContent: string, newContent: string, allowAccessToPluginRoot: boolean = false, requiresConfirmation: boolean = true): Promise<TFile | Error> {
const file: TAbstractFile | null = this.vaultService.getAbstractFileByPath(filePath, allowAccessToPluginRoot);
let fileToPatch: TFile;
if (file instanceof TFile) {
fileToPatch = file;
} else {
// if the file doesn't exist we may as well create it even though this is just a patch operation
const result = await this.writeFile(filePath, "", allowAccessToPluginRoot, requiresConfirmation);
if (result instanceof Error) {
return result;
}
fileToPatch = result;
}
return await this.vaultService.patch(fileToPatch, oldContent, newContent, allowAccessToPluginRoot, requiresConfirmation);
}
public async deleteFile(filePath: string, allowAccessToPluginRoot: boolean = false, requiresConfirmation: boolean = true): Promise<Error | void> {
const file: TAbstractFile | null = this.vaultService.getAbstractFileByPath(filePath, allowAccessToPluginRoot);
if (!file) {
return Exception.new(`File does not exist: ${filePath}`);
}
return await this.vaultService.delete(file, allowAccessToPluginRoot, requiresConfirmation);
}
public async moveFile(sourcePath: string, destinationPath: string, allowAccessToPluginRoot: boolean = false): Promise<void | Error> {
return await this.vaultService.move(sourcePath, destinationPath, allowAccessToPluginRoot);
}
public async listFilesInDirectory(dirPath: string, recursive: boolean = true, allowAccessToPluginRoot: boolean = false): Promise<TFile[]> {
return await this.vaultService.listFilesInDirectory(dirPath, recursive, allowAccessToPluginRoot);
}
public async listFoldersInDirectory(dirPath: string, recursive: boolean = true, allowAccessToPluginRoot: boolean = false): Promise<TFolder[]> {
return await this.vaultService.listFoldersInDirectory(dirPath, recursive, allowAccessToPluginRoot);
}
public async listDirectoryContents(dirPath: string, recursive: boolean = true, allowAccessToPluginRoot: boolean = false): Promise<TAbstractFile[]> {
return await this.vaultService.listDirectoryContents(dirPath, recursive, allowAccessToPluginRoot);
}
public async readObjectFromFile(filePath: string, allowAccessToPluginRoot: boolean = false): Promise<Record<string, unknown> | Error> {
const file: TAbstractFile | null = this.vaultService.getAbstractFileByPath(filePath, allowAccessToPluginRoot);
if (file && file instanceof TFile) {
const result = await this.vaultService.read(file, allowAccessToPluginRoot);
return typeof result === "string" ? JSON.parse(result) as Record<string, unknown> : result;
}
return Exception.new(`File not found: ${filePath}`);
}
public async writeObjectToFile(filePath: string, data: object, allowAccessToPluginRoot: boolean = false, requiresConfirmation: boolean = true): Promise<TFile | Error> {
const file: TAbstractFile | null = this.vaultService.getAbstractFileByPath(filePath, allowAccessToPluginRoot);
let result: TFile | Error;
if (file && file instanceof TFile) {
result = await this.vaultService.modify(file, JSON.stringify(data, null, 4), allowAccessToPluginRoot, requiresConfirmation);
}
else {
result = await this.vaultService.create(filePath, JSON.stringify(data, null, 4), allowAccessToPluginRoot, requiresConfirmation);
}
return result;
}
public async searchVaultFiles(searchTerm: string, allowAccessToPluginRoot: boolean = false): Promise<ISearchMatch[]> {
return await this.vaultService.searchVaultFiles(searchTerm, allowAccessToPluginRoot);
}
}