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)
This commit is contained in:
Andrew Beal 2025-11-01 11:43:32 +00:00
parent 2b5f977478
commit 72ddbba72e
16 changed files with 897 additions and 324 deletions

View file

@ -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) {

View file

@ -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"]
}
}

View file

@ -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"]

View file

@ -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",
}

View file

@ -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<object> {
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<object> {
const files: TAbstractFile[] = await this.fileSystemService.listDirectoryContents(path, recursive);
return files.map(file => ({
type: file instanceof TFile ? "file" : "directory",
path: file.path
}));
}
}

View file

@ -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<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<object | null> {
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<ISearchMatch[]> {
return await this.vaultService.searchVaultFiles(searchTerm);
public async searchVaultFiles(searchTerm: string, allowAccessToPluginRoot: boolean = false): Promise<ISearchMatch[]> {
return await this.vaultService.searchVaultFiles(searchTerm, allowAccessToPluginRoot);
}
}

View file

@ -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));
}
}

View file

@ -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);

View file

@ -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<TAbstractFile[]> {
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<TFile[]> {
const dir: TAbstractFile | null = this.getAbstractFileByPath(this.sanitiserService.sanitize(dirPath), allowAccessToPluginRoot);
public async listFilesInDirectory(path: string, recursive: boolean = true, allowAccessToPluginRoot: boolean = false): Promise<TFile[]> {
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<ISearchMatch[]> {
public async listFoldersInDirectory(path: string, recursive: boolean = true, allowAccessToPluginRoot: boolean = false): Promise<TFolder[]> {
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<ISearchMatch[]> {
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[] = [];

View file

@ -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');
});
});

View file

@ -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('/');
});
});

View file

@ -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;

View file

@ -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([]);
});

View file

@ -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<StatusBarService>(Services.StatusBarService).removeStatusBarMessage();
DeregisterAllServices();
}
async activateView() {

559
package-lock.json generated
View file

@ -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",

View file

@ -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",