Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-05-23 - RAG Indexing Performance
**Learning:** The `RagIndexingService` uses `FileUploader` from `gemini-utils`, which scans all files. `FileUploader` relies on the provided `FileSystemAdapter` to compute hashes. To optimize indexing of unchanged files, we cannot modify `FileUploader` logic directly. Instead, we must optimize the `FileSystemAdapter.computeHash` implementation to use a cache.
**Action:** When optimizing file operations controlled by external libraries (like `gemini-utils`), look for adapter interfaces (like `FileSystemAdapter`) where you can inject caching or optimized logic.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,16 @@ Let the AI actively work with your vault through tool calling capabilities.
- **Context Files:** Add specific notes as persistent context
- **Session Configuration:** Override model, temperature, and prompt per session
- **Safety Features:** System folders are protected from modifications
- **Tool Permission System**:
- Granular control over which tools the agent can use
- **Trusted Mode**: Optional setting to allow file modifications without constant confirmation prompts (Use with caution!)
- **New Power Tools**:
- `update_frontmatter`: Safely modify note properties (status, tags, dates) without rewriting content. Critical for "Bases" and "Projects" workflows.
- `append_content`: Efficiently add text to the end of notes (great for logs and journals).
- `fetch_url`: Alias for Web Fetch, ensuring compatibility with standard agent prompts.
- **RAG (Retrieval Augmented Generation)**:
- Index your vault for semantic search
- Retrieve relevant context based on user queries

**Example Commands:**

Expand Down
24 changes: 23 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export interface ObsidianGeminiSettings {
loopDetectionEnabled: boolean;
loopDetectionThreshold: number;
loopDetectionTimeWindowSeconds: number;
// Trusted Mode
alwaysAllowReadWrite: boolean;
// V4 upgrade tracking
hasSeenV4Welcome: boolean;
// Version tracking for update notifications
Expand Down Expand Up @@ -106,6 +108,8 @@ const DEFAULT_SETTINGS: ObsidianGeminiSettings = {
loopDetectionEnabled: true,
loopDetectionThreshold: 3,
loopDetectionTimeWindowSeconds: 30,
// Trusted Mode
alwaysAllowReadWrite: false,
// V4 upgrade tracking
hasSeenV4Welcome: false,
// Version tracking for update notifications
Expand Down Expand Up @@ -532,11 +536,29 @@ export default class ObsidianGemini extends Plugin {
this.toolRegistry.registerTool(tool);
}

// Register extended vault tools (Frontmatter & Append)
// Dynamically import to avoid circular dependencies if any
const { UpdateFrontmatterTool, AppendContentTool } = await import(
'./tools/vault-tools-extended'
);
this.toolRegistry.registerTool(new UpdateFrontmatterTool());
this.toolRegistry.registerTool(new AppendContentTool());

// Register web tools (Google Search and Web Fetch)
const { getWebTools } = await import('./tools/web-tools');
const webTools = getWebTools();
for (const tool of webTools) {
this.toolRegistry.registerTool(tool);
// Register fetch_url alias for WebFetchTool
// This is required for compatibility with AGENTS.md which expects 'fetch_url'
if (tool.name === 'web_fetch') {
// We need to cast to any or instantiate a new class to set the name
// Since setToolName is not on the interface, we'll try to clone it or instantiate new
// Easier approach: Use the WebFetchTool class directly if possible, or just re-register
// However, getWebTools returns instances.
const { WebFetchTool } = await import('./tools/web-fetch-tool');
this.toolRegistry.registerTool(new WebFetchTool(this).setName('fetch_url'));
}
}

// Register memory tools
Expand Down Expand Up @@ -655,7 +677,7 @@ export default class ObsidianGemini extends Plugin {

// Clean up partial initialization
if (this.ragIndexing) {
await this.ragIndexing.destroy().catch(() => {});
await this.ragIndexing.destroy().catch(() => { });
this.ragIndexing = null;
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/services/obsidian-file-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class ObsidianVaultAdapter implements FileSystemAdapter {
private historyFolder: string;
private includeAttachments: boolean;
private logError?: (message: string, ...args: unknown[]) => void;
private hashCacheProvider?: (path: string, mtime: number) => string | null;

constructor(options: {
vault: Vault;
Expand All @@ -26,13 +27,15 @@ export class ObsidianVaultAdapter implements FileSystemAdapter {
historyFolder?: string;
includeAttachments?: boolean;
logError?: (message: string, ...args: unknown[]) => void;
hashCacheProvider?: (path: string, mtime: number) => string | null;
}) {
this.vault = options.vault;
this.metadataCache = options.metadataCache;
this.excludeFolders = options.excludeFolders || [];
this.historyFolder = options.historyFolder || '';
this.includeAttachments = options.includeAttachments || false;
this.logError = options.logError;
this.hashCacheProvider = options.hashCacheProvider;
}

/**
Expand Down Expand Up @@ -176,6 +179,14 @@ export class ObsidianVaultAdapter implements FileSystemAdapter {
return '';
}

// Check cache first
if (this.hashCacheProvider) {
const cachedHash = this.hashCacheProvider(filePath, file.stat.mtime);
if (cachedHash) {
return cachedHash;
}
}

try {
const content = await this.vault.readBinary(file);
const hashBuffer = await crypto.subtle.digest('SHA-256', content);
Expand Down
14 changes: 14 additions & 0 deletions src/services/rag-indexing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface IndexedFileEntry {
resourceName: string; // Gemini file resource name
contentHash: string; // SHA-256 hash for reliable change detection
lastIndexed: number; // Timestamp
mtime?: number; // Last modification time of the file
}

/**
Expand Down Expand Up @@ -183,6 +184,13 @@ export class RagIndexingService {
historyFolder: this.plugin.settings.historyFolder,
includeAttachments: this.plugin.settings.ragIndexing.includeAttachments,
logError: (msg, ...args) => this.plugin.logger.error(msg, ...args),
hashCacheProvider: (path, mtime) => {
const entry = this.cache?.files[path];
if (entry && entry.mtime === mtime) {
return entry.contentHash;
}
return null;
},
});

// Create file uploader with logger
Expand Down Expand Up @@ -1180,10 +1188,12 @@ export class RagIndexingService {
// Update cache for newly indexed file
if (this.cache && event.currentFile && this.vaultAdapter) {
const contentHash = await this.vaultAdapter.computeHash(event.currentFile);
const file = this.plugin.app.vault.getAbstractFileByPath(event.currentFile);
this.cache.files[event.currentFile] = {
resourceName: storeName, // Store name as reference (individual doc names not available)
contentHash,
lastIndexed: Date.now(),
mtime: file instanceof TFile ? file.stat.mtime : undefined,
};
// Track last indexed file for resume capability
this.cache.lastIndexedFile = event.currentFile;
Expand All @@ -1209,10 +1219,12 @@ export class RagIndexingService {
// Skipped files are already in cache (unchanged), ensure they're tracked
if (this.cache && event.currentFile && !this.cache.files[event.currentFile] && this.vaultAdapter) {
const contentHash = await this.vaultAdapter.computeHash(event.currentFile);
const file = this.plugin.app.vault.getAbstractFileByPath(event.currentFile);
this.cache.files[event.currentFile] = {
resourceName: storeName,
contentHash,
lastIndexed: Date.now(),
mtime: file instanceof TFile ? file.stat.mtime : undefined,
};
}
// Incremental cache save for durability (count skipped files too)
Expand Down Expand Up @@ -1498,6 +1510,7 @@ export class RagIndexingService {
resourceName: storeName,
contentHash: content.hash,
lastIndexed: Date.now(),
mtime: file.stat.mtime,
};
}
// Incremental cache save for durability
Expand All @@ -1519,6 +1532,7 @@ export class RagIndexingService {
resourceName: storeName,
contentHash: content.hash,
lastIndexed: Date.now(),
mtime: file.stat.mtime,
};
}
// Incremental cache save for durability
Expand Down
57 changes: 33 additions & 24 deletions src/tools/execution-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,37 +81,46 @@ export class ToolExecutionEngine {
// Check if confirmation is required
const requiresConfirmation = this.registry.requiresConfirmation(toolCall.name, context);

if (requiresConfirmation) {
// Check if this tool is allowed without confirmation for this session
const isAllowedWithoutConfirmation = view?.isToolAllowedWithoutConfirmation?.(toolCall.name) || false;

if (!isAllowedWithoutConfirmation) {
// Update progress to show waiting for confirmation
const toolDisplay = tool.displayName || tool.name;
const confirmationMessage = `Waiting for confirmation: ${toolDisplay}`;
view?.updateProgress?.(confirmationMessage, 'waiting');

const result = await this.requestUserConfirmation(tool, toolCall.arguments, view);

// Update progress back to tool execution
view?.updateProgress?.(`Executing: ${toolDisplay}`, 'tool');

if (!result.confirmed) {
return {
success: false,
error: 'User declined tool execution',
};
}
// If user allowed this action without future confirmation
if (result.allowWithoutConfirmation && view) {
view.allowToolWithoutConfirmation(toolCall.name);
if (requiresConfirmation) {
// Check if Trusted Mode is enabled
if (this.plugin.settings.alwaysAllowReadWrite) {
context.plugin.logger.log(
`[Trusted Mode] Bypassing confirmation for ${toolCall.name}`
);
} else {
// Check if this tool is allowed without confirmation for this session
const isAllowedWithoutConfirmation =
view?.isToolAllowedWithoutConfirmation?.(toolCall.name) || false;

if (!isAllowedWithoutConfirmation) {
// Update progress to show waiting for confirmation
const toolDisplay = tool.displayName || tool.name;
const confirmationMessage = `Waiting for confirmation: ${toolDisplay}`;
view?.updateProgress?.(confirmationMessage, 'waiting');

const result = await this.requestUserConfirmation(tool, toolCall.arguments, view);

// Update progress back to tool execution
view?.updateProgress?.(`Executing: ${toolDisplay}`, 'tool');

if (!result.confirmed) {
return {
success: false,
error: 'User declined tool execution',
};
}
// If user allowed this action without future confirmation
if (result.allowWithoutConfirmation && view) {
view.allowToolWithoutConfirmation(toolCall.name);
}
}
}
}

// Show execution notification (disabled - now shown in chat UI)
// const executionNotice = new Notice(`Executing ${tool.name}...`, 0);
const executionNotice = { hide: () => {} }; // Dummy object for compatibility
const executionNotice = { hide: () => { } }; // Dummy object for compatibility

try {
// Record the execution attempt
Expand Down
Loading