From 72ddbba72eb2c28e6f045a0dfab650d649ec07d8 Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Sat, 1 Nov 2025 11:43:32 +0000 Subject: [PATCH] feat: add ListVaultFiles function and improve search behavior - Add new ListVaultFiles AI function for listing vault contents - Remove auto-fallback to listing all files when search returns 0 results - Improve SearchVaultFiles description to clarify it searches content - Add listFoldersInDirectory and listDirectoryContents methods to VaultService - Update SanitiserService to normalize empty strings to "/" (vault root) - Add allowAccessToPluginRoot parameter to searchVaultFiles - Update tests to reflect new search behavior and list operations - Add DeregisterAllServices call in plugin onunload - Update dependencies (@google/genai, svelte, vitest, eslint) --- .../AIFunctionDefinitions.ts | 4 +- .../Functions/ListVaultFiles.ts | 31 + .../Functions/SearchVaultFiles.ts | 18 +- Enums/AIFunction.ts | 3 +- Services/AIFunctionService.ts | 25 +- Services/FileSystemService.ts | 12 +- Services/SanitiserService.ts | 45 +- Services/VaultCacheService.ts | 9 +- Services/VaultService.ts | 57 +- __tests__/Services/AIFunctionService.test.ts | 49 +- __tests__/Services/SanitiserService.test.ts | 9 +- __tests__/Services/VaultCacheService.test.ts | 12 +- __tests__/Services/VaultService.test.ts | 377 +++++++++--- main.ts | 3 +- package-lock.json | 559 +++++++++++++----- package.json | 8 +- 16 files changed, 897 insertions(+), 324 deletions(-) create mode 100644 AIClasses/FunctionDefinitions/Functions/ListVaultFiles.ts diff --git a/AIClasses/FunctionDefinitions/AIFunctionDefinitions.ts b/AIClasses/FunctionDefinitions/AIFunctionDefinitions.ts index 2561376..742fe08 100644 --- a/AIClasses/FunctionDefinitions/AIFunctionDefinitions.ts +++ b/AIClasses/FunctionDefinitions/AIFunctionDefinitions.ts @@ -4,12 +4,14 @@ import { ReadVaultFiles } from "./Functions/ReadVaultFiles"; import { WriteVaultFile } from "./Functions/WriteVaultFile"; import { DeleteVaultFiles } from "./Functions/DeleteVaultFiles"; import { MoveVaultFiles } from "./Functions/MoveVaultFiles"; +import { ListVaultFiles } from "./Functions/ListVaultFiles"; export class AIFunctionDefinitions { public getQueryActions(destructive: boolean): IAIFunctionDefinition[] { let actions = [ SearchVaultFiles, - ReadVaultFiles + ReadVaultFiles, + ListVaultFiles ]; if (destructive) { diff --git a/AIClasses/FunctionDefinitions/Functions/ListVaultFiles.ts b/AIClasses/FunctionDefinitions/Functions/ListVaultFiles.ts new file mode 100644 index 0000000..1464aad --- /dev/null +++ b/AIClasses/FunctionDefinitions/Functions/ListVaultFiles.ts @@ -0,0 +1,31 @@ +import { AIFunction } from "Enums/AIFunction"; +import type { IAIFunctionDefinition } from "../IAIFunctionDefinition"; + +export const ListVaultFiles: IAIFunctionDefinition = { + name: AIFunction.ListVaultFiles, + description: `Lists files and directories in the vault's directory structure. + Returns a structured view of the vault's organization including file names, paths, and directory hierarchy. + Use this function when you need to: + - List files in a specific directory or the entire vault + - Get an overview of vault organization and structure + - Browse available files and folders + - Understand how notes are organized`, + parameters: { + type: "object", + properties: { + path: { + type: "string", + description: `The directory path to list. Use "/" for the vault root. Specify a subdirectory path to list contents of that specific folder (e.g., 'Projects/2024' or 'Daily Notes'). Path should be relative to vault root.`, + }, + recursive: { + type: "boolean", + description: "When true, recursively lists all files and subdirectories in a tree structure. When false, only lists immediate children of the specified directory.", + }, + user_message: { + type: "string", + description: "A short message to be displayed to the user explaining what directory is being listed. Example: 'Browsing vault structure' or 'Listing files in Daily Notes folder'" + } + }, + required: ["path", "recursive", "user_message"] + } + } \ No newline at end of file diff --git a/AIClasses/FunctionDefinitions/Functions/SearchVaultFiles.ts b/AIClasses/FunctionDefinitions/Functions/SearchVaultFiles.ts index b936b76..1c48ba2 100644 --- a/AIClasses/FunctionDefinitions/Functions/SearchVaultFiles.ts +++ b/AIClasses/FunctionDefinitions/Functions/SearchVaultFiles.ts @@ -5,27 +5,21 @@ export const SearchVaultFiles: IAIFunctionDefinition = { name: AIFunction.SearchVaultFiles, description: `Searches the content of all vault files using regex pattern matching. Returns files containing the search term with contextual snippets showing where matches appear. - - **IMPORTANT: When a search returns 0 results, a complete list of all vault files will be automatically returned.** - This allows you to verify the search scope and attempt alternative search strategies. - - Use an empty search term to retrieve a complete list of all vault files explicitly. - Use this function when you need to: - - Retrieve a full list of files in the users vault - - Search for specific concepts, keywords, or text within notes - - Locate content that matches a pattern or phrase - - Answer questions about what the user has written about a topic`, + - Find specific concepts, keywords, or text within note contents + - Locate content matching a pattern or phrase + - Answer questions about what the user has written about a topic + - Search across both file names and file contents simultaneously`, parameters: { type: "object", properties: { search_term: { type: "string", - description: "The regex pattern to search for in vault files. Supports both simple text (e.g., 'todo') and regex patterns (e.g., '(urgent|important)'). The search is performed on both file names and content." + description: `The regex pattern to search for in vault files. Supports both simple text searches (e.g., 'meeting notes', 'project alpha') and advanced regex patterns (e.g., '(urgent|important)', '\\d{4}-\\d{2}-\\d{2}' for dates). The search is case-insensitive and performed on both file names and content. Use empty string "" to return all vault files.` }, user_message: { type: "string", - description: "A short message to be displayed to the user that explains the action being taken" + description: "A short message to be displayed to the user explaining what is being searched for. Example: 'Searching for notes about project meetings' or 'Finding files containing todo items'" } }, required: ["search_term", "user_message"] diff --git a/Enums/AIFunction.ts b/Enums/AIFunction.ts index c5515cc..818f476 100644 --- a/Enums/AIFunction.ts +++ b/Enums/AIFunction.ts @@ -4,7 +4,8 @@ export enum AIFunction { WriteVaultFile = "write_vault_file", DeleteVaultFiles = "delete_vault_files", MoveVaultFiles = "move_vault_files", + ListVaultFiles = "list_vault_files", // only used by gemini - RequestWebSearch = "request_web_search" + RequestWebSearch = "request_web_search", } \ No newline at end of file diff --git a/Services/AIFunctionService.ts b/Services/AIFunctionService.ts index 01eaddf..0f444ea 100644 --- a/Services/AIFunctionService.ts +++ b/Services/AIFunctionService.ts @@ -5,8 +5,7 @@ import { AIFunction } from "Enums/AIFunction"; import { AIFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionResponse"; import type { AIFunctionCall } from "AIClasses/AIFunctionCall"; import type { ISearchMatch } from "../Helpers/SearchTypes"; -import { normalizePath, TFile } from "obsidian"; -import { Path } from "Enums/Path"; +import { normalizePath, TAbstractFile, TFile } from "obsidian"; export class AIFunctionService { @@ -29,6 +28,9 @@ export class AIFunctionService { case AIFunction.MoveVaultFiles: return new AIFunctionResponse(functionCall.name, await this.moveVaultFiles(functionCall.arguments.source_paths, functionCall.arguments.destination_paths), functionCall.toolId); + case AIFunction.ListVaultFiles: + return new AIFunctionResponse(functionCall.name, await this.ListVaultFiles(functionCall.arguments.path, functionCall.arguments.recursive), functionCall.toolId); + // this is only used by gemini case AIFunction.RequestWebSearch: return new AIFunctionResponse(functionCall.name, {}, functionCall.toolId) @@ -47,16 +49,7 @@ export class AIFunctionService { private async searchVaultFiles(searchTerm: string): Promise { const matches: ISearchMatch[] = searchTerm.trim() === "" ? [] : await this.fileSystemService.searchVaultFiles(searchTerm); - if (matches.length === 0) { - const files: TFile[] = await this.fileSystemService.listFilesInDirectory(Path.Root); - return files.map((file) => ({ - name: file.basename, - path: file.path - })); - } - - return matches.map((match) => ({ - name: match.file.basename, + return matches.map(match => ({ path: match.file.path, snippets: match.snippets.map((snippet) => ({ text: snippet.text, @@ -117,4 +110,12 @@ export class AIFunctionService { return { results }; } + + private async ListVaultFiles(path: string, recursive: boolean): Promise { + const files: TAbstractFile[] = await this.fileSystemService.listDirectoryContents(path, recursive); + return files.map(file => ({ + type: file instanceof TFile ? "file" : "directory", + path: file.path + })); + } } \ No newline at end of file diff --git a/Services/FileSystemService.ts b/Services/FileSystemService.ts index 0e72151..713a455 100644 --- a/Services/FileSystemService.ts +++ b/Services/FileSystemService.ts @@ -64,6 +64,14 @@ export class FileSystemService { return await this.vaultService.listFilesInDirectory(dirPath, recursive, allowAccessToPluginRoot); } + public async listFoldersInDirectory(dirPath: string, recursive: boolean = true, allowAccessToPluginRoot: boolean = false): Promise { + return await this.vaultService.listFoldersInDirectory(dirPath, recursive, allowAccessToPluginRoot); + } + + public async listDirectoryContents(dirPath: string, recursive: boolean = true, allowAccessToPluginRoot: boolean = false): Promise { + return await this.vaultService.listDirectoryContents(dirPath, recursive, allowAccessToPluginRoot); + } + public async readObjectFromFile(filePath: string, allowAccessToPluginRoot: boolean = false): Promise { const file: TAbstractFile | null = this.vaultService.getAbstractFileByPath(filePath, allowAccessToPluginRoot); if (file && file instanceof TFile) { @@ -93,7 +101,7 @@ export class FileSystemService { } } - public async searchVaultFiles(searchTerm: string): Promise { - return await this.vaultService.searchVaultFiles(searchTerm); + public async searchVaultFiles(searchTerm: string, allowAccessToPluginRoot: boolean = false): Promise { + return await this.vaultService.searchVaultFiles(searchTerm, allowAccessToPluginRoot); } } \ No newline at end of file diff --git a/Services/SanitiserService.ts b/Services/SanitiserService.ts index b0c026f..3e30911 100644 --- a/Services/SanitiserService.ts +++ b/Services/SanitiserService.ts @@ -21,20 +21,25 @@ export class SanitiserService { */ public sanitize(input: string, options: ISanitizeOptions = {}): string { // Type check - if (typeof input !== 'string') { - throw new Error('Input must be a string'); + if (typeof input !== "string") { + throw new Error("Input must be a string"); + } + + // Normalize empty string to "/" for vault root + if (input.trim() === "") { + return "/"; } // Default options - const replacement = options.replacement || ''; - const outputSeparator = options.separator || '/'; + const replacement = options.replacement || ""; + const outputSeparator = options.separator || "/"; // Detect if this is an absolute path - const isAbsolute = input.startsWith('/') || /^[a-zA-Z]:[\\\/]/.test(input); + const isAbsolute = input.startsWith("/") || /^[a-zA-Z]:[\\\/]/.test(input); // Detect Windows drive letter (e.g., C:) const driveMatch = input.match(/^([a-zA-Z]:)[\\\/]/); - const driveLetter = driveMatch ? driveMatch[1] : ''; + const driveLetter = driveMatch ? driveMatch[1] : ""; // Split by both forward and back slashes // Note: Forward slashes and backslashes are treated as path separators, @@ -45,22 +50,22 @@ export class SanitiserService { // But keep track of whether we started with a slash segments = segments.filter((seg, index) => { // Keep the first segment even if empty (for absolute paths like /home/...) - if (index === 0 && seg === '' && isAbsolute && !driveLetter) { + if (index === 0 && seg === "" && isAbsolute && !driveLetter) { return true; } - return seg !== ''; + return seg !== ""; }); // Sanitize each segment const sanitizedSegments = segments.map((segment, index) => { - // Don't sanitize the drive letter (first segment if it's something like "C:") - if (index === 0 && driveLetter && segment === driveLetter.replace(':', '')) { + // Don"t sanitize the drive letter (first segment if it"s something like "C:") + if (index === 0 && driveLetter && segment === driveLetter.replace(":", "")) { return driveLetter; } // For the first empty segment of an absolute path, keep it empty - if (index === 0 && segment === '' && isAbsolute) { - return ''; + if (index === 0 && segment === "" && isAbsolute) { + return ""; } return this.sanitizeSegment(segment, replacement); @@ -76,7 +81,7 @@ export class SanitiserService { // Truncate the entire path if needed (paths can be up to 4096 bytes on most systems) // But for individual filenames, most systems have a 255-byte limit - // We'll apply the 255-byte limit to the filename part only + // We"ll apply the 255-byte limit to the filename part only const lastSeparatorIndex = result.lastIndexOf(outputSeparator); if (lastSeparatorIndex !== -1) { const directory = result.substring(0, lastSeparatorIndex + 1); @@ -84,7 +89,7 @@ export class SanitiserService { const truncatedFilename = this.truncateToByteLength(filename, 255); result = directory + truncatedFilename; } else { - // If there's no separator, the whole thing is a filename + // If there"s no separator, the whole thing is a filename result = this.truncateToByteLength(result, 255); } @@ -98,7 +103,7 @@ export class SanitiserService { * @returns Sanitized segment, or a fallback value if the result would be empty */ private sanitizeSegment(segment: string, replacement: string): string { - if (!segment || segment === '') { + if (!segment || segment === "") { return segment; } @@ -111,8 +116,8 @@ export class SanitiserService { // Handle case where sanitization results in an empty string // This can happen with names like "...", "CON", or strings containing only illegal characters - if (sanitized === '') { - sanitized = 'unnamed'; + if (sanitized === "") { + sanitized = "unnamed"; } return sanitized; @@ -143,13 +148,13 @@ export class SanitiserService { } // Decode with fatal mode to ensure we get a valid UTF-8 string - // If decoding fails (which shouldn't happen with our boundary logic), fall back to safe decode + // If decoding fails (which shouldn"t happen with our boundary logic), fall back to safe decode try { - const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }); + const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); return decoder.decode(encoded.slice(0, truncateAt)); } catch { // Fallback: use non-fatal mode if something unexpected happens - const decoder = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true }); + const decoder = new TextDecoder("utf-8", { fatal: false, ignoreBOM: true }); return decoder.decode(encoded.slice(0, truncateAt)); } } diff --git a/Services/VaultCacheService.ts b/Services/VaultCacheService.ts index 392d885..6730aea 100644 --- a/Services/VaultCacheService.ts +++ b/Services/VaultCacheService.ts @@ -6,6 +6,7 @@ import { FileEvent } from "Enums/FileEvent"; import { getAllTags, MetadataCache, TFile, TFolder } from "obsidian"; import { FileTagMapping } from "Helpers/FileTagMapping"; import * as fuzzysort from "fuzzysort"; +import { Path } from "Enums/Path"; // Note that 'files' actually refers to both directories and files (Obsidian naming) @@ -37,9 +38,9 @@ export class VaultCacheService { this.metaDataCache = this.plugin.app.metadataCache; this.registerFileEvents(); - this.plugin.app.metadataCache.on("resolved", () => { + this.plugin.app.metadataCache.on("resolved", async () => { if (!this.initialised) { - this.setupCaches(); + await this.setupCaches(); this.initialised = true; } }); @@ -135,8 +136,8 @@ export class VaultCacheService { }); } - private setupCaches() { - this.vaultService.listVaultContents().forEach(file => { + private async setupCaches() { + (await this.vaultService.listDirectoryContents(Path.Root)).forEach(file => { if (file instanceof TFile) { this.files.set(file.path, file); this.cacheTags(file); diff --git a/Services/VaultService.ts b/Services/VaultService.ts index bcf58aa..3ee8f02 100644 --- a/Services/VaultService.ts +++ b/Services/VaultService.ts @@ -133,18 +133,17 @@ export class VaultService { return await this.vault.createFolder(path); } - public listVaultContents(allowAccessToPluginRoot: boolean = false) { - const files = this.vault.getFiles() - .filter(file => !this.isExclusion(file.path, allowAccessToPluginRoot)); + public async listDirectoryContents(path: string, recursive: boolean = true, allowAccessToPluginRoot: boolean = false): Promise { + const sanitisedPath = this.sanitiserService.sanitize(path); - const folders = this.vault.getAllFolders() - .filter(folder => !this.isExclusion(folder.path, allowAccessToPluginRoot)); + const files = await this.listFilesInDirectory(sanitisedPath, recursive, allowAccessToPluginRoot); + const folders = await this.listFoldersInDirectory(sanitisedPath, recursive, allowAccessToPluginRoot); return [...files, ...folders] as TAbstractFile[]; } - public async listFilesInDirectory(dirPath: string, recursive: boolean = true, allowAccessToPluginRoot: boolean = false): Promise { - const dir: TAbstractFile | null = this.getAbstractFileByPath(this.sanitiserService.sanitize(dirPath), allowAccessToPluginRoot); + public async listFilesInDirectory(path: string, recursive: boolean = true, allowAccessToPluginRoot: boolean = false): Promise { + const dir: TAbstractFile | null = this.getAbstractFileByPath(this.sanitiserService.sanitize(path), allowAccessToPluginRoot); if (dir == null || !(dir instanceof TFolder)) { return []; @@ -153,19 +152,47 @@ export class VaultService { let files: TFile[] = []; for (let child of dir.children) { if (child instanceof TFile) { - files.push(child); + if (!this.isExclusion(child.path, allowAccessToPluginRoot)) { + files.push(child); + } } else if (child instanceof TFolder && recursive) { - const childFiles = await this.listFilesInDirectory(child.path, recursive, allowAccessToPluginRoot); - files = files.concat(childFiles); + if (!this.isExclusion(child.path, allowAccessToPluginRoot)) { + const childFiles = await this.listFilesInDirectory(child.path, recursive, allowAccessToPluginRoot); + files = files.concat(childFiles); + } } } - // if an excluded file or directory is present we just filter it rather than - // reporting it since this function could touch a large part of the vault - return files.filter(file => !this.isExclusion(file.path, allowAccessToPluginRoot)); + return files; } - public async searchVaultFiles(searchTerm: string): Promise { + public async listFoldersInDirectory(path: string, recursive: boolean = true, allowAccessToPluginRoot: boolean = false): Promise { + const dir: TAbstractFile | null = this.getAbstractFileByPath(this.sanitiserService.sanitize(path), allowAccessToPluginRoot); + + if (dir == null || !(dir instanceof TFolder)) { + return []; + } + + let folders: TFolder[] = []; + for (let child of dir.children) { + if (!(child instanceof TFolder)) { + continue; + } + + if (!this.isExclusion(child.path, allowAccessToPluginRoot)) { + folders.push(child); + + if (recursive) { + const childFolders = await this.listFoldersInDirectory(child.path, recursive, allowAccessToPluginRoot); + folders = folders.concat(childFolders); + } + } + } + + return folders; + } + + public async searchVaultFiles(searchTerm: string, allowAccessToPluginRoot: boolean = false): Promise { let regex: RegExp; try { regex = new RegExp(searchTerm, "ig"); @@ -173,7 +200,7 @@ export class VaultService { regex = new RegExp(escapeRegex(searchTerm), "ig"); } - const files: TFile[] = await this.listFilesInDirectory(Path.Root); + const files: TFile[] = await this.listFilesInDirectory(Path.Root, true, allowAccessToPluginRoot); const allMatches: ISearchMatch[] = []; diff --git a/__tests__/Services/AIFunctionService.test.ts b/__tests__/Services/AIFunctionService.test.ts index 86cea03..fa6f3e3 100644 --- a/__tests__/Services/AIFunctionService.test.ts +++ b/__tests__/Services/AIFunctionService.test.ts @@ -89,7 +89,6 @@ describe('AIFunctionService - Integration Tests', () => { expect(result.toolId).toBe('tool_1'); expect(result.response).toEqual([ { - name: 'test', path: 'notes/test.md', snippets: [ { text: 'This is a test note', matchPosition: 10 }, @@ -97,7 +96,6 @@ describe('AIFunctionService - Integration Tests', () => { ] }, { - name: 'guide', path: 'docs/guide.md', snippets: [ { text: 'Guide for testing', matchPosition: 0 } @@ -106,55 +104,30 @@ describe('AIFunctionService - Integration Tests', () => { ]); }); - it('should return all files when search term is empty', async () => { - const mockFiles = [ - createMockFile('file1.md', 'file1'), - createMockFile('file2.md', 'file2'), - createMockFile('folder/file3.md', 'file3') - ]; - - mockFileSystemService.searchVaultFiles.mockResolvedValue([]); - mockFileSystemService.listFilesInDirectory.mockResolvedValue(mockFiles); - + it('should return empty array when search term is empty', async () => { const result = await service.performAIFunction({ name: AIFunction.SearchVaultFiles, arguments: { search_term: '' }, toolId: 'tool_2' } as any); - expect(mockFileSystemService.listFilesInDirectory).toHaveBeenCalledWith('/'); - expect(result.response).toEqual([ - { name: 'file1', path: 'file1.md' }, - { name: 'file2', path: 'file2.md' }, - { name: 'file3', path: 'folder/file3.md' } - ]); + // Empty search terms return empty results + expect(result.response).toEqual([]); }); - it('should return all files when search term is whitespace', async () => { - const mockFiles = [createMockFile('test.md', 'test')]; - - mockFileSystemService.searchVaultFiles.mockResolvedValue([]); - mockFileSystemService.listFilesInDirectory.mockResolvedValue(mockFiles); - + it('should return empty array when search term is whitespace', async () => { const result = await service.performAIFunction({ name: AIFunction.SearchVaultFiles, arguments: { search_term: ' ' }, toolId: 'tool_3' } as any); - expect(result.response).toEqual([ - { name: 'test', path: 'test.md' } - ]); + // Whitespace search terms return empty results (after trim) + expect(result.response).toEqual([]); }); - it('should return all files when no matches found', async () => { - const mockFiles = [ - createMockFile('file1.md', 'file1'), - createMockFile('file2.md', 'file2') - ]; - + it('should return empty array when no matches found', async () => { mockFileSystemService.searchVaultFiles.mockResolvedValue([]); - mockFileSystemService.listFilesInDirectory.mockResolvedValue(mockFiles); const result = await service.performAIFunction({ name: AIFunction.SearchVaultFiles, @@ -162,10 +135,8 @@ describe('AIFunctionService - Integration Tests', () => { toolId: 'tool_4' } as any); - expect(result.response).toEqual([ - { name: 'file1', path: 'file1.md' }, - { name: 'file2', path: 'file2.md' } - ]); + // No matches returns empty results + expect(result.response).toEqual([]); }); it('should handle single match', async () => { @@ -185,7 +156,7 @@ describe('AIFunctionService - Integration Tests', () => { } as any); expect(result.response).toHaveLength(1); - expect(result.response[0].name).toBe('single'); + expect(result.response[0].path).toBe('single.md'); }); }); diff --git a/__tests__/Services/SanitiserService.test.ts b/__tests__/Services/SanitiserService.test.ts index 758d4b7..f137135 100644 --- a/__tests__/Services/SanitiserService.test.ts +++ b/__tests__/Services/SanitiserService.test.ts @@ -30,9 +30,14 @@ describe('SanitiserService', () => { expect(() => service.sanitize(undefined as any)).toThrow('Input must be a string'); }); - it('should handle empty string', () => { + it('should normalize empty string to vault root', () => { const result = service.sanitize(''); - expect(result).toBe(''); + expect(result).toBe('/'); + }); + + it('should normalize whitespace-only string to vault root', () => { + const result = service.sanitize(' '); + expect(result).toBe('/'); }); }); diff --git a/__tests__/Services/VaultCacheService.test.ts b/__tests__/Services/VaultCacheService.test.ts index 3c9a00b..df404fd 100644 --- a/__tests__/Services/VaultCacheService.test.ts +++ b/__tests__/Services/VaultCacheService.test.ts @@ -121,7 +121,7 @@ describe('VaultCacheService - Integration Tests', () => { registerFileEvents: vi.fn((handler: any) => { fileEventHandler = handler; }), - listVaultContents: vi.fn(() => []), + listDirectoryContents: vi.fn(async () => []), getAbstractFileByPath: vi.fn((path: string) => { // By default, allow all folders (return a mock folder) // This maintains existing test behavior @@ -188,7 +188,7 @@ describe('VaultCacheService - Integration Tests', () => { const mockVaultServiceWithContent = { registerFileEvents: mockRegisterEvents, - listVaultContents: mockListVaultContents + listDirectoryContents: mockListVaultContents }; RegisterSingleton(Services.AIAgentPlugin, mockPluginWithMetadata as any); @@ -613,7 +613,7 @@ describe('VaultCacheService - Integration Tests', () => { registerFileEvents: vi.fn((handler: any) => { fileEventHandler = handler; }), - listVaultContents: vi.fn(() => []), + listDirectoryContents: vi.fn(async () => []), getAbstractFileByPath: vi.fn((path: string) => { // Return null for excluded paths (simulating VaultService exclusion logic) if (path.startsWith('AI Agent')) { @@ -656,7 +656,7 @@ describe('VaultCacheService - Integration Tests', () => { registerFileEvents: vi.fn((handler: any) => { fileEventHandler = handler; }), - listVaultContents: vi.fn(() => []), + listDirectoryContents: vi.fn(async () => []), getAbstractFileByPath: vi.fn((path: string) => { // Return null for excluded paths if (path.startsWith('private/')) { @@ -687,7 +687,7 @@ describe('VaultCacheService - Integration Tests', () => { registerFileEvents: vi.fn((handler: any) => { fileEventHandler = handler; }), - listVaultContents: vi.fn(() => []), + listDirectoryContents: vi.fn(async () => []), getAbstractFileByPath: vi.fn((path: string, allowAccessToPluginRoot: boolean) => { // First call (public-folder) should succeed if (path === 'public-folder') { @@ -735,7 +735,7 @@ describe('VaultCacheService - Integration Tests', () => { registerFileEvents: vi.fn((handler: any) => { fileEventHandler = handler; }), - listVaultContents: vi.fn(() => []), + listDirectoryContents: vi.fn(async () => []), getAbstractFileByPath: vi.fn((path: string) => { if (path.startsWith('AI Agent')) { return null; diff --git a/__tests__/Services/VaultService.test.ts b/__tests__/Services/VaultService.test.ts index 33dddfa..4ac16eb 100644 --- a/__tests__/Services/VaultService.test.ts +++ b/__tests__/Services/VaultService.test.ts @@ -605,6 +605,129 @@ describe('VaultService - Integration Tests', () => { }); }); + describe('listFoldersInDirectory', () => { + it('should list all folders in directory non-recursively', async () => { + const folder1 = createMockFolder('parent/folder1', []); + const folder2 = createMockFolder('parent/folder2', []); + const parentFolder = createMockFolder('parent', [folder1, folder2]); + + mockVault.getAbstractFileByPath.mockReturnValue(parentFolder); + + const result = await vaultService.listFoldersInDirectory('parent', false); + + expect(result).toHaveLength(2); + expect(result).toContain(folder1); + expect(result).toContain(folder2); + }); + + it('should list all folders recursively', async () => { + const subfolder1 = createMockFolder('parent/child/subfolder1', []); + const childFolder = createMockFolder('parent/child', [subfolder1]); + const folder1 = createMockFolder('parent/folder1', []); + const parentFolder = createMockFolder('parent', [folder1, childFolder]); + + mockVault.getAbstractFileByPath.mockImplementation((path: string) => { + if (path === 'parent') return parentFolder; + if (path === 'parent/folder1') return folder1; + if (path === 'parent/child') return childFolder; + return null; + }); + + const result = await vaultService.listFoldersInDirectory('parent', true); + + expect(result).toHaveLength(3); + expect(result).toContain(folder1); + expect(result).toContain(childFolder); + expect(result).toContain(subfolder1); + }); + + it('should filter out excluded folders from results', async () => { + const publicFolder = createMockFolder('parent/public', []); + const privateFolder = createMockFolder('parent/private', []); + const parentFolder = createMockFolder('parent', [publicFolder, privateFolder]); + + mockVault.getAbstractFileByPath.mockReturnValue(parentFolder); + mockPluginSettings.exclusions = ['**/private']; + + const result = await vaultService.listFoldersInDirectory('parent', false); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe('parent/public'); + }); + + it('should return empty array when directory does not exist', async () => { + mockVault.getAbstractFileByPath.mockReturnValue(null); + + const result = await vaultService.listFoldersInDirectory('nonexistent'); + + expect(result).toEqual([]); + }); + + it('should return empty array when path is a file, not a directory', async () => { + const mockFile = createMockFile('file.md'); + mockVault.getAbstractFileByPath.mockReturnValue(mockFile); + + const result = await vaultService.listFoldersInDirectory('file.md'); + + expect(result).toEqual([]); + }); + + it('should respect allowAccessToPluginRoot parameter', async () => { + const agentFolder = createMockFolder('AI Agent', []); + const notesFolder = createMockFolder('notes', []); + const rootFolder = createMockFolder('/', [agentFolder, notesFolder]); + + mockVault.getAbstractFileByPath.mockImplementation((path: string) => { + if (path === '/' || path === '') return rootFolder; + // AI Agent blocked when allowAccessToPluginRoot = false + if (path === 'AI Agent') return null; + if (path === 'notes') return notesFolder; + return null; + }); + + // With allowAccessToPluginRoot = false (should exclude AI Agent) + const result1 = await vaultService.listFoldersInDirectory('/', true, false); + expect(result1.some((folder) => folder.path === 'AI Agent')).toBe(false); + expect(result1.some((folder) => folder.path === 'notes')).toBe(true); + + // With allowAccessToPluginRoot = true (should include AI Agent) + mockVault.getAbstractFileByPath.mockImplementation((path: string) => { + if (path === '/' || path === '') return rootFolder; + if (path === 'AI Agent') return agentFolder; // Now allowed + if (path === 'notes') return notesFolder; + return null; + }); + + const result2 = await vaultService.listFoldersInDirectory('/', true, true); + expect(result2.some((folder) => folder.path === 'AI Agent')).toBe(true); + expect(result2.some((folder) => folder.path === 'notes')).toBe(true); + }); + + it('should not recurse into excluded folders', async () => { + const excludedSubfolder = createMockFolder('parent/excluded/sub', []); + const excludedFolder = createMockFolder('parent/excluded', [excludedSubfolder]); + const allowedFolder = createMockFolder('parent/allowed', []); + const parentFolder = createMockFolder('parent', [excludedFolder, allowedFolder]); + + mockVault.getAbstractFileByPath.mockImplementation((path: string) => { + if (path === 'parent') return parentFolder; + if (path === 'parent/excluded') return excludedFolder; // Should not be accessed + if (path === 'parent/allowed') return allowedFolder; + return null; + }); + + // Use pattern that matches the folder itself and its contents + mockPluginSettings.exclusions = ['parent/excluded/**', 'parent/excluded']; + + const result = await vaultService.listFoldersInDirectory('parent', true); + + // Should not include excluded folder or its subfolders + expect(result.some((folder) => folder.path === 'parent/excluded')).toBe(false); + expect(result.some((folder) => folder.path === 'parent/excluded/sub')).toBe(false); + expect(result.some((folder) => folder.path === 'parent/allowed')).toBe(true); + }); + }); + describe('searchVaultFiles', () => { it('should find matches in file content', async () => { const file1 = createMockFile('note1.md'); @@ -715,6 +838,121 @@ describe('VaultService - Integration Tests', () => { expect(results.length).toBeGreaterThan(0); // Should find all three variants }); + + it('should not log errors when vault contains excluded directories', async () => { + // Setup: Create a vault structure with excluded "AI Agent" directory + const normalFile = createMockFile('notes/document.md'); + const excludedFile = createMockFile('AI Agent/secret.md'); + const notesFolder = createMockFolder('notes', [normalFile]); + const excludedFolder = createMockFolder('AI Agent', [excludedFile]); + const rootFolder = createMockFolder('/', [normalFile, notesFolder, excludedFolder]); + + mockVault.getAbstractFileByPath.mockImplementation((path: string) => { + if (path === '/' || path === '') return rootFolder; + if (path === 'notes') return notesFolder; + if (path === 'notes/document.md') return normalFile; + // AI Agent directory should be blocked by getAbstractFileByPath + if (path === 'AI Agent') return null; + return null; + }); + + mockVault.cachedRead.mockResolvedValue('test content'); + + // Spy on console.error + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + // Execute search + const results = await vaultService.searchVaultFiles('test'); + + // Should not log exclusion errors during normal search operation + expect(consoleErrorSpy).not.toHaveBeenCalledWith( + expect.stringContaining('Plugin attempted to retrieve a file that is in the exclusions list') + ); + + // Should still return results from non-excluded files + expect(results.length).toBeGreaterThan(0); + + consoleErrorSpy.mockRestore(); + }); + + it('should respect allowAccessToPluginRoot parameter when true', async () => { + // Setup: Create vault with AI Agent directory + const agentFile = createMockFile('AI Agent/notes.md'); + const normalFile = createMockFile('normal.md'); + const agentFolder = createMockFolder('AI Agent', [agentFile]); + const rootFolder = createMockFolder('/', [normalFile, agentFolder]); + + mockVault.getAbstractFileByPath.mockImplementation((path: string) => { + if (path === '/' || path === '') return rootFolder; + if (path === 'AI Agent') return agentFolder; + if (path === 'AI Agent/notes.md') return agentFile; + if (path === 'normal.md') return normalFile; + return null; + }); + + mockVault.cachedRead.mockResolvedValue('searchable content'); + + // Search with allowAccessToPluginRoot = true + const results = await vaultService.searchVaultFiles('searchable', true); + + // Should include files from AI Agent directory + const paths = results.map(r => r.file.path); + expect(paths).toContain('AI Agent/notes.md'); + expect(paths).toContain('normal.md'); + }); + + it('should exclude AI Agent directory when allowAccessToPluginRoot is false', async () => { + // Setup: Create vault with AI Agent directory + const agentFile = createMockFile('AI Agent/notes.md'); + const normalFile = createMockFile('normal.md'); + const agentFolder = createMockFolder('AI Agent', [agentFile]); + const rootFolder = createMockFolder('/', [normalFile, agentFolder]); + + mockVault.getAbstractFileByPath.mockImplementation((path: string) => { + if (path === '/' || path === '') return rootFolder; + // When allowAccessToPluginRoot is false, AI Agent should be blocked + if (path === 'AI Agent') return null; + if (path === 'normal.md') return normalFile; + return null; + }); + + mockVault.cachedRead.mockResolvedValue('searchable content'); + + // Search with allowAccessToPluginRoot = false (default) + const results = await vaultService.searchVaultFiles('searchable', false); + + // Should NOT include files from AI Agent directory + const paths = results.map(r => r.file.path); + expect(paths).not.toContain('AI Agent/notes.md'); + expect(paths).toContain('normal.md'); + }); + + it('should pass allowAccessToPluginRoot to listFilesInDirectory', async () => { + const file = createMockFile('test.md'); + const folder = createMockFolder('/', [file]); + + mockVault.getAbstractFileByPath.mockReturnValue(folder); + mockVault.cachedRead.mockResolvedValue('content'); + + // Create a spy on listFilesInDirectory + const listFilesSpy = vi.spyOn(vaultService, 'listFilesInDirectory'); + + // Call with allowAccessToPluginRoot = true + await vaultService.searchVaultFiles('test', true); + + // Verify listFilesInDirectory was called with the correct parameter + expect(listFilesSpy).toHaveBeenCalledWith(Path.Root, true, true); + + listFilesSpy.mockClear(); + + // Call with allowAccessToPluginRoot = false + await vaultService.searchVaultFiles('test', false); + + // Verify listFilesInDirectory was called with the correct parameter + expect(listFilesSpy).toHaveBeenCalledWith(Path.Root, true, false); + + listFilesSpy.mockRestore(); + }); }); describe('isExclusion (private method behavior)', () => { @@ -866,99 +1104,104 @@ describe('VaultService - Integration Tests', () => { }); }); - describe('listVaultContents', () => { - it('should return all files and folders when no exclusions', () => { - const files = [ - createMockFile('note1.md'), - createMockFile('note2.md') - ]; - const folders = [ - createMockFolder('folder1'), - createMockFolder('folder2') - ]; + describe('listDirectoryContents', () => { + it('should return all files and folders when no exclusions', async () => { + const file1 = createMockFile('note1.md'); + const file2 = createMockFile('note2.md'); + const folder1 = createMockFolder('folder1', []); + const folder2 = createMockFolder('folder2', []); + const rootFolder = createMockFolder('/', [file1, file2, folder1, folder2]); - mockVault.getFiles.mockReturnValue(files); - mockVault.getAllFolders.mockReturnValue(folders); + mockVault.getAbstractFileByPath.mockImplementation((path: string) => { + if (path === '/' || path === '') return rootFolder; + if (path === 'folder1') return folder1; + if (path === 'folder2') return folder2; + return null; + }); - const result = vaultService.listVaultContents(); + const result = await vaultService.listDirectoryContents(Path.Root); expect(result).toHaveLength(4); - expect(result).toEqual(expect.arrayContaining([...files, ...folders])); + expect(result).toEqual(expect.arrayContaining([file1, file2, folder1, folder2])); }); - it('should filter out excluded files and folders', () => { - const files = [ - createMockFile('public/note.md'), - createMockFile('AI Agent/conversation.md'), - createMockFile('private/secret.md') - ]; - const folders = [ - createMockFolder('public'), - createMockFolder('AI Agent'), - createMockFolder('private') - ]; + it('should filter out excluded files and folders', async () => { + const publicNote = createMockFile('public/note.md'); + const agentNote = createMockFile('AI Agent/conversation.md'); + const privateNote = createMockFile('private/secret.md'); + const publicFolder = createMockFolder('public', [publicNote]); + const agentFolder = createMockFolder('AI Agent', [agentNote]); + const privateFolder = createMockFolder('private', [privateNote]); + const rootFolder = createMockFolder('/', [publicNote, agentNote, privateNote, publicFolder, agentFolder, privateFolder]); + + mockVault.getAbstractFileByPath.mockImplementation((path: string) => { + if (path === '/' || path === '') return rootFolder; + if (path === 'public') return publicFolder; + if (path === 'AI Agent') return null; // Excluded by default + if (path === 'private') return privateFolder; + return null; + }); - mockVault.getFiles.mockReturnValue(files); - mockVault.getAllFolders.mockReturnValue(folders); mockPluginSettings.exclusions = ['private/**']; - const result = vaultService.listVaultContents(false); + const result = await vaultService.listDirectoryContents(Path.Root, true, false); - // Should have: public/note.md, public folder, AI Agent folder, private folder - // Should exclude: AI Agent/** (files inside - default), private/** (files inside) - // Note: The pattern 'private/**' matches files inside but not the folder itself - // Similarly 'AI Agent/**' (the default) matches files inside but not the folder - expect(result.some((item) => item.path === 'public/note.md')).toBe(true); - expect(result.some((item) => item.path === 'public')).toBe(true); - expect(result.some((item) => item.path === 'AI Agent/conversation.md')).toBe(false); - expect(result.some((item) => item.path === 'private/secret.md')).toBe(false); + // Should include: public/note.md and public folder + // Should exclude: AI Agent folder (default exclusion), private/** content + expect(result.some((item: any) => item.path === 'public/note.md')).toBe(true); + expect(result.some((item: any) => item.path === 'public')).toBe(true); + expect(result.some((item: any) => item.path === 'AI Agent/conversation.md')).toBe(false); + expect(result.some((item: any) => item.path === 'private/secret.md')).toBe(false); + expect(result.some((item: any) => item.path === 'private')).toBe(true); // Folder itself not excluded by 'private/**' }); - it('should exclude the AI Agent directory itself from folder listings', () => { - const files = [ - createMockFile('public/note.md') - ]; - const folders = [ - createMockFolder('public'), - createMockFolder('AI Agent'), - createMockFolder('notes') - ]; + it('should exclude the AI Agent directory itself from folder listings', async () => { + const publicNote = createMockFile('public/note.md'); + const publicFolder = createMockFolder('public', [publicNote]); + const agentFolder = createMockFolder('AI Agent', []); + const notesFolder = createMockFolder('notes', []); + const rootFolder = createMockFolder('/', [publicNote, publicFolder, agentFolder, notesFolder]); - mockVault.getFiles.mockReturnValue(files); - mockVault.getAllFolders.mockReturnValue(folders); + mockVault.getAbstractFileByPath.mockImplementation((path: string) => { + if (path === '/' || path === '') return rootFolder; + if (path === 'public') return publicFolder; + if (path === 'AI Agent') return null; // Excluded by default + if (path === 'notes') return notesFolder; + return null; + }); - const result = vaultService.listVaultContents(false); + const result = await vaultService.listDirectoryContents(Path.Root, true, false); // AI Agent directory itself should be excluded - expect(result.some((item) => item.path === 'AI Agent')).toBe(false); + expect(result.some((item: any) => item.path === 'AI Agent')).toBe(false); // Other folders should be included - expect(result.some((item) => item.path === 'public')).toBe(true); - expect(result.some((item) => item.path === 'notes')).toBe(true); + expect(result.some((item: any) => item.path === 'public')).toBe(true); + expect(result.some((item: any) => item.path === 'notes')).toBe(true); }); - it('should include AI Agent directory when allowAccessToPluginRoot is true', () => { - const files = [ - createMockFile('note.md'), - createMockFile('AI Agent/conversation.md') - ]; - const folders = [ - createMockFolder('AI Agent') - ]; + it('should include AI Agent directory when allowAccessToPluginRoot is true', async () => { + const note = createMockFile('note.md'); + const agentNote = createMockFile('AI Agent/conversation.md'); + const agentFolder = createMockFolder('AI Agent', [agentNote]); + const rootFolder = createMockFolder('/', [note, agentFolder]); - mockVault.getFiles.mockReturnValue(files); - mockVault.getAllFolders.mockReturnValue(folders); + mockVault.getAbstractFileByPath.mockImplementation((path: string) => { + if (path === '/' || path === '') return rootFolder; + if (path === 'AI Agent') return agentFolder; // Allowed with flag + return null; + }); - const result = vaultService.listVaultContents(true); + const result = await vaultService.listDirectoryContents(Path.Root, true, true); expect(result).toHaveLength(3); - expect(result.some((item) => item.path === 'AI Agent/conversation.md')).toBe(true); + expect(result.some((item: any) => item.path === 'AI Agent/conversation.md')).toBe(true); }); - it('should return empty array when vault is empty', () => { - mockVault.getFiles.mockReturnValue([]); - mockVault.getAllFolders.mockReturnValue([]); + it('should return empty array when vault is empty', async () => { + const emptyRoot = createMockFolder('/', []); + mockVault.getAbstractFileByPath.mockReturnValue(emptyRoot); - const result = vaultService.listVaultContents(); + const result = await vaultService.listDirectoryContents(Path.Root); expect(result).toEqual([]); }); diff --git a/main.ts b/main.ts index 64deec0..2886bb0 100644 --- a/main.ts +++ b/main.ts @@ -5,7 +5,7 @@ import { RegisterAiProvider, RegisterDependencies } from 'Services/ServiceRegist import { AIAgentSettingTab } from 'AIAgentSettingTab'; import { Services } from 'Services/Services'; import type { StatusBarService } from 'Services/StatusBarService'; -import { Resolve } from 'Services/DependencyService'; +import { DeregisterAllServices, Resolve } from 'Services/DependencyService'; interface IAIAgentSettings { model: string; @@ -54,6 +54,7 @@ export default class AIAgentPlugin extends Plugin { async onunload() { Resolve(Services.StatusBarService).removeStatusBarMessage(); + DeregisterAllServices(); } async activateView() { diff --git a/package-lock.json b/package-lock.json index b457b1c..754f6e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.68.0", - "@google/genai": "^1.27.0", + "@google/genai": "^1.28.0", "@shikijs/rehype": "^3.14.0", "core-js": "^3.46.0", "express": "^5.1.0", @@ -46,18 +46,18 @@ "@types/node": "^24.9.2", "@typescript-eslint/eslint-plugin": "8.46.2", "@typescript-eslint/parser": "8.46.2", - "@vitest/ui": "^4.0.5", + "@vitest/ui": "^4.0.6", "builtin-modules": "5.0.0", "esbuild": "^0.25.11", "esbuild-svelte": "^0.9.3", "happy-dom": "^20.0.10", "obsidian": "latest", - "svelte": "^5.43.0", + "svelte": "^5.43.2", "svelte-check": "^4.3.3", "svelte-preprocess": "^6.0.3", "tslib": "2.8.1", "typescript": "^5.9.3", - "vitest": "^4.0.5" + "vitest": "^4.0.6" } }, "node_modules/@anthropic-ai/sdk": { @@ -666,7 +666,7 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-helpers/node_modules/@eslint/core": { + "node_modules/@eslint/core": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", @@ -680,20 +680,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@eslint/eslintrc": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", @@ -757,9 +743,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", - "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", + "version": "9.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz", + "integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==", "dev": true, "license": "MIT", "peer": true, @@ -796,24 +782,10 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@google/genai": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.27.0.tgz", - "integrity": "sha512-sveeQqwyzO/U5kOjo3EflF1rf7v0ZGprrjPGmeT6V5u22IUTcA4wBFxW+q1n7hOX0M1iWR3944MImoNPOM+zsA==", + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.28.0.tgz", + "integrity": "sha512-0pfZ1EWQsM9kINsL+mFKJvpzM6NRHS9t360S1MzKq4JtIwTj/RbsPpC/K5wpKiPy9PC+J+bsz/9gvaL51++KrA==", "license": "Apache-2.0", "dependencies": { "google-auth-library": "^10.3.0", @@ -887,6 +859,23 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -978,6 +967,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -1924,16 +1923,16 @@ "license": "ISC" }, "node_modules/@vitest/expect": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.5.tgz", - "integrity": "sha512-DJctLVlKoddvP/G389oGmKWNG6GD9frm2FPXARziU80Rjo7SIYxQzb2YFzmQ4fVD3Q5utUYY8nUmWrqsuIlIXQ==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.6.tgz", + "integrity": "sha512-5j8UUlBVhOjhj4lR2Nt9sEV8b4WtbcYh8vnfhTNA2Kn5+smtevzjNq+xlBuVhnFGXiyPPNzGrOVvmyHWkS5QGg==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.5", - "@vitest/utils": "4.0.5", + "@vitest/spy": "4.0.6", + "@vitest/utils": "4.0.6", "chai": "^6.0.1", "tinyrainbow": "^3.0.3" }, @@ -1942,13 +1941,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.5.tgz", - "integrity": "sha512-iYHIy72LfbK+mL5W8zXROp6oOcJKXWeKcNjcPPsqoa18qIEDrhB6/Z08o0wRajTd6SSSDNw8NCSIHVNOMpz0mw==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.6.tgz", + "integrity": "sha512-3COEIew5HqdzBFEYN9+u0dT3i/NCwppLnO1HkjGfAP1Vs3vti1Hxm/MvcbC4DAn3Szo1M7M3otiAaT83jvqIjA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.5", + "@vitest/spy": "4.0.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.19" }, @@ -1969,9 +1968,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.5.tgz", - "integrity": "sha512-t1T/sSdsYyNc5AZl0EMeD0jW9cpJe2cODP0R++ZQe1kTkpgrwEfxGFR/yCG4w8ZybizbXRTHU7lE8sTDD/QsGw==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.6.tgz", + "integrity": "sha512-4vptgNkLIA1W1Nn5X4x8rLJBzPiJwnPc+awKtfBE5hNMVsoAl/JCCPPzNrbf+L4NKgklsis5Yp2gYa+XAS442g==", "dev": true, "license": "MIT", "dependencies": { @@ -1982,13 +1981,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.5.tgz", - "integrity": "sha512-CQVVe+YEeKSiFBD5gBAmRDQglm4PnMBYzeTmt06t5iWtsUN9StQeeKhYCea/oaqBYilf8sARG6fSctUcEL/UmQ==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.6.tgz", + "integrity": "sha512-trPk5qpd7Jj+AiLZbV/e+KiiaGXZ8ECsRxtnPnCrJr9OW2mLB72Cb824IXgxVz/mVU3Aj4VebY+tDTPn++j1Og==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.5", + "@vitest/utils": "4.0.6", "pathe": "^2.0.3" }, "funding": { @@ -1996,13 +1995,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.5.tgz", - "integrity": "sha512-jfmSAeR6xYNEvcD+/RxFGA1bzpqHtkVhgxo2cxXia+Q3xX7m6GpZij07rz+WyQcA/xEGn4eIS1OItkMyWsGBmQ==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.6.tgz", + "integrity": "sha512-PaYLt7n2YzuvxhulDDu6c9EosiRuIE+FI2ECKs6yvHyhoga+2TBWI8dwBjs+IeuQaMtZTfioa9tj3uZb7nev1g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.5", + "@vitest/pretty-format": "4.0.6", "magic-string": "^0.30.19", "pathe": "^2.0.3" }, @@ -2011,9 +2010,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.5.tgz", - "integrity": "sha512-TUmVQpAQign7r8+EnZsgTF3vY9BdGofTUge1rGNbnHn2IN3FChiQoT9lrPz7A7AVUZJU2LAZXl4v66HhsNMhoA==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.6.tgz", + "integrity": "sha512-g9jTUYPV1LtRPRCQfhbMintW7BTQz1n6WXYQYRQ25qkyffA4bjVXjkROokZnv7t07OqfaFKw1lPzqKGk1hmNuQ==", "dev": true, "license": "MIT", "funding": { @@ -2021,13 +2020,13 @@ } }, "node_modules/@vitest/ui": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.5.tgz", - "integrity": "sha512-msuwwWsWSKKOid91osirrm0hDUFojT9wde4GSefCj7BHz9SrXbFJSbrrKKEow0AmGhQi/k2FxIWnVifioWbMVg==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.6.tgz", + "integrity": "sha512-1ekpBsYNUm0Xv/0YsTvoSRmiRkmzz9Pma7qQ3Ui76sg2gwp2/ewSWqx4W/HfaN5dF0E8iBbidFo1wGaeqXYIrQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.5", + "@vitest/utils": "4.0.6", "fflate": "^0.8.2", "flatted": "^3.3.3", "pathe": "^2.0.3", @@ -2039,17 +2038,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "4.0.5" + "vitest": "4.0.6" } }, "node_modules/@vitest/utils": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.5.tgz", - "integrity": "sha512-V5RndUgCB5/AfNvK9zxGCrRs99IrPYtMTIdUzJMMFs9nrmE5JXExIEfjVtUteyTRiLfCm+dCRMHf/Uu7Mm8/dg==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.6.tgz", + "integrity": "sha512-bG43VS3iYKrMIZXBo+y8Pti0O7uNju3KvNn6DrQWhQQKcLavMB+0NZfO1/QBAEbq0MaQ3QjNsnnXlGQvsh0Z6A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.5", + "@vitest/pretty-format": "4.0.6", "tinyrainbow": "^3.0.3" }, "funding": { @@ -2123,7 +2122,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2133,9 +2131,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2215,7 +2211,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -2271,7 +2266,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -2474,9 +2468,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2488,9 +2480,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/comma-separated-tokens": { "version": "2.0.3", @@ -2581,9 +2571,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2692,6 +2680,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -2707,6 +2701,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/emojilib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", @@ -2861,9 +2861,9 @@ } }, "node_modules/eslint": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", - "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", + "version": "9.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.0.tgz", + "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "dev": true, "license": "MIT", "peer": true, @@ -2871,11 +2871,11 @@ "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.1", - "@eslint/core": "^0.16.0", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.38.0", - "@eslint/plugin-kit": "^0.4.0", + "@eslint/js": "9.39.0", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3057,9 +3057,9 @@ } }, "node_modules/esrap": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.1.tgz", - "integrity": "sha512-ebTT9B6lOtZGMgJ3o5r12wBacHctG7oEWazIda8UlPfA3HD/Wrv8FdXoVo73vzdpwCxNyXjPauyN2bbJzMkB9A==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.2.tgz", + "integrity": "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -3369,6 +3369,22 @@ "dev": true, "license": "ISC" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -3438,23 +3454,24 @@ "license": "MIT" }, "node_modules/gaxios": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.2.tgz", - "integrity": "sha512-/Szrn8nr+2TsQT1Gp8iIe/BEytJmbyfrbFh419DfGQSkEgNEhbPi7JRJuughjkTzPWgU9gBQf5AVu3DbHt0OXA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", "license": "Apache-2.0", "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" }, "engines": { "node": ">=18" } }, "node_modules/gcp-metadata": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.1.tgz", - "integrity": "sha512-dTCcAe9fRQf06ELwel6lWWFrEbstwjUBYEhr5VRGoC+iPDZQucHppCowaIp8b8v92tU1G4X4H3b/Y6zXZxkMsQ==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", "license": "Apache-2.0", "dependencies": { "gaxios": "^7.0.0", @@ -3508,6 +3525,26 @@ "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", "license": "ISC" }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3537,9 +3574,9 @@ } }, "node_modules/google-auth-library": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.4.2.tgz", - "integrity": "sha512-EKiQasw6aEdxSovPEf1oBxCEvxjFamZ6MPaVOSPXZMnqKFLo+rrYjHyjKlFfZcXiKi9qAH6cutr5WRqqa1jKhg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", @@ -3555,9 +3592,9 @@ } }, "node_modules/google-logging-utils": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.1.tgz", - "integrity": "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.2.tgz", + "integrity": "sha512-YsFPGVgDFf4IzSwbwIR0iaFJQFmR5Jp7V1WuYSjuRgAm9yWqsMhKE9YPlL+wvFLnc/wMiFV4SQUD9Y/JMpxIxQ==", "license": "Apache-2.0", "engines": { "node": ">=14" @@ -4141,6 +4178,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4205,9 +4251,22 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } }, "node_modules/js-tokens": { "version": "4.0.0", @@ -4395,6 +4454,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -5508,7 +5573,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -5520,6 +5584,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -5657,9 +5730,9 @@ } }, "node_modules/obsidian": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.10.2.tgz", - "integrity": "sha512-bX03YCHf06OTzI/D+QK71ajCPCmwr/cjxzlVXjQa10DjK5iHRWhtJJpp83arSCyayFMp23u+UHcY7hxcEx2Mvg==", + "version": "1.10.2-1", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.10.2-1.tgz", + "integrity": "sha512-nScksVndWZDz0e/OAPa7Yo0aFaNuv6amyHg2L6MvlxfTJD6TfmkprVWhHjiVvUIGwcdvZRy6LGkn/v8D2E9jng==", "dev": true, "license": "MIT", "dependencies": { @@ -5762,6 +5835,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5850,13 +5929,27 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", @@ -6453,6 +6546,21 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.52.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", @@ -6634,9 +6742,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -6648,9 +6754,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -6750,6 +6854,18 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -6820,6 +6936,56 @@ "dev": true, "license": "MIT" }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -6834,6 +7000,46 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6871,9 +7077,9 @@ } }, "node_modules/svelte": { - "version": "5.43.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.0.tgz", - "integrity": "sha512-1sRxVbgJAB+UGzwkc3GUoiBSzEOf0jqzccMaVoI2+pI+kASUe9qubslxace8+Mzhqw19k4syTA5niCIJwfXpOA==", + "version": "5.43.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.2.tgz", + "integrity": "sha512-ro1umEzX8rT5JpCmlf0PPv7ncD8MdVob9e18bhwqTKNoLjS8kDvhVpaoYVPc+qMwDAOfcwJtyY7ZFSDbOaNPgA==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -7563,19 +7769,19 @@ } }, "node_modules/vitest": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.5.tgz", - "integrity": "sha512-4H+J28MI5oeYgGg3h5BFSkQ1g/2GKK1IR8oorH3a6EQQbb7CwjbnyBjH4PGxw9/6vpwAPNzaeUMp4Js4WJmdXQ==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.6.tgz", + "integrity": "sha512-gR7INfiVRwnEOkCk47faros/9McCZMp5LM+OMNWGLaDBSvJxIzwjgNFufkuePBNaesGRnLmNfW+ddbUJRZn0nQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.5", - "@vitest/mocker": "4.0.5", - "@vitest/pretty-format": "4.0.5", - "@vitest/runner": "4.0.5", - "@vitest/snapshot": "4.0.5", - "@vitest/spy": "4.0.5", - "@vitest/utils": "4.0.5", + "@vitest/expect": "4.0.6", + "@vitest/mocker": "4.0.6", + "@vitest/pretty-format": "4.0.6", + "@vitest/runner": "4.0.6", + "@vitest/snapshot": "4.0.6", + "@vitest/spy": "4.0.6", + "@vitest/utils": "4.0.6", "debug": "^4.4.3", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", @@ -7603,10 +7809,10 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.5", - "@vitest/browser-preview": "4.0.5", - "@vitest/browser-webdriverio": "4.0.5", - "@vitest/ui": "4.0.5", + "@vitest/browser-playwright": "4.0.6", + "@vitest/browser-preview": "4.0.6", + "@vitest/browser-webdriverio": "4.0.6", + "@vitest/ui": "4.0.6", "happy-dom": "*", "jsdom": "*" }, @@ -7694,9 +7900,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", - "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -7735,6 +7939,85 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 2c5063a..85cb8e1 100644 --- a/package.json +++ b/package.json @@ -22,22 +22,22 @@ "@types/node": "^24.9.2", "@typescript-eslint/eslint-plugin": "8.46.2", "@typescript-eslint/parser": "8.46.2", - "@vitest/ui": "^4.0.5", + "@vitest/ui": "^4.0.6", "builtin-modules": "5.0.0", "esbuild": "^0.25.11", "esbuild-svelte": "^0.9.3", "happy-dom": "^20.0.10", "obsidian": "latest", - "svelte": "^5.43.0", + "svelte": "^5.43.2", "svelte-check": "^4.3.3", "svelte-preprocess": "^6.0.3", "tslib": "2.8.1", "typescript": "^5.9.3", - "vitest": "^4.0.5" + "vitest": "^4.0.6" }, "dependencies": { "@anthropic-ai/sdk": "^0.68.0", - "@google/genai": "^1.27.0", + "@google/genai": "^1.28.0", "@shikijs/rehype": "^3.14.0", "core-js": "^3.46.0", "express": "^5.1.0",