Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92f5e1c33a | |||
| 60cd6bfaec | |||
| d7c049e978 | |||
| c61f66928f | |||
| 6b6795bb00 | |||
| b17205c2f9 | |||
| f459cbac67 | |||
| 8b7a90d2a8 | |||
| 3b50754386 | |||
| e1e05e82ae | |||
| 9c1c11df5a | |||
| 0fe118f9e6 | |||
| b520a20444 | |||
| 187fb07934 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '20'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -10,6 +10,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.1.3] - 2025-12-16
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Sentence Case**: Fixed remaining UI text violations
|
||||||
|
- `main.ts`: "Toggle MCP Server" → "Toggle MCP server"
|
||||||
|
- `settings.ts`: "Authentication & Configuration" → "Authentication & configuration"
|
||||||
|
- `settings.ts`: "UI Notifications" → "UI notifications"
|
||||||
|
|
||||||
|
- **Promise Handling**: Improved async/promise patterns per Obsidian guidelines
|
||||||
|
- `main.ts`: Changed `async onunload()` to synchronous with `void this.stopServer()`
|
||||||
|
- `routes.ts`: Wrapped async Express handler with void IIFE pattern
|
||||||
|
- `mcp-server.ts`: Promise rejection now always uses Error instance
|
||||||
|
|
||||||
|
- **Async/Await Cleanup**: Removed `async` from 7 methods that contained no `await`:
|
||||||
|
- `mcp-server.ts`: `handleInitialize`, `handleListTools`
|
||||||
|
- `vault-tools.ts`: `getVaultInfo`, `listNotes`, `createFileMetadataWithFrontmatter`, `exists`, `resolveWikilink`
|
||||||
|
- `link-utils.ts`: `validateLinks`
|
||||||
|
|
||||||
|
- **Type Safety**: Replaced `any` types with `Record<string, unknown>` and removed eslint-disable directives
|
||||||
|
- `mcp-server.ts`: Tool arguments type
|
||||||
|
- `tools/index.ts`: `callTool` args parameter
|
||||||
|
- `notifications.ts`: `args` interface field, `showToolCall` parameter, `formatArgs` parameter
|
||||||
|
|
||||||
|
- **Import Statements**: Eliminated forbidden `require()` imports
|
||||||
|
- `crypto-adapter.ts`: Replaced `require('crypto')` with `globalThis.crypto`
|
||||||
|
- `encryption-utils.ts`: Replaced bare `require('electron')` with `window.require` pattern
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated test mocks to match new synchronous method signatures and import patterns
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.1.2] - 2025-11-15
|
## [1.1.2] - 2025-11-15
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
636
docs/plans/2025-12-16-obsidian-code-review-fixes.md
Normal file
636
docs/plans/2025-12-16-obsidian-code-review-fixes.md
Normal file
@@ -0,0 +1,636 @@
|
|||||||
|
# Obsidian Plugin Code Review Fixes Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Fix all required issues from the Nov 16, 2025 ObsidianReviewBot code review to unblock plugin submission approval.
|
||||||
|
|
||||||
|
**Architecture:** Systematic file-by-file fixes addressing: sentence case UI text, async/await cleanup, eslint directive removal, require() to ES6 import conversion, and promise handling improvements.
|
||||||
|
|
||||||
|
**Tech Stack:** TypeScript, Obsidian API, ESLint
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Fix Sentence Case in main.ts
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/main.ts:45`
|
||||||
|
|
||||||
|
**Step 1: Fix ribbon icon tooltip**
|
||||||
|
|
||||||
|
Change line 45 from:
|
||||||
|
```typescript
|
||||||
|
this.addRibbonIcon('server', 'Toggle MCP Server', async () => {
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
this.addRibbonIcon('server', 'Toggle MCP server', async () => {
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Fix onunload promise issue (lines 96-98)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
async onunload() {
|
||||||
|
await this.stopServer();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
onunload() {
|
||||||
|
void this.stopServer();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify build**
|
||||||
|
|
||||||
|
Run: `npm run build`
|
||||||
|
Expected: No errors
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/main.ts
|
||||||
|
git commit -m "fix: sentence case and onunload promise in main.ts"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Fix Sentence Case in settings.ts
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/settings.ts:209,319`
|
||||||
|
|
||||||
|
**Step 1: Fix authentication section header (line 209)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
authSummary.setText('Authentication & Configuration');
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
authSummary.setText('Authentication & configuration');
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Fix notifications section header (line 319)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
notifSummary.setText('UI Notifications');
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
notifSummary.setText('UI notifications');
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify build**
|
||||||
|
|
||||||
|
Run: `npm run build`
|
||||||
|
Expected: No errors
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/settings.ts
|
||||||
|
git commit -m "fix: sentence case for section headers in settings.ts"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Fix mcp-server.ts Issues
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/server/mcp-server.ts:57,70,77-79,117`
|
||||||
|
|
||||||
|
**Step 1: Remove async from handleInitialize (line 57)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
private async handleInitialize(_params: JSONRPCParams): Promise<InitializeResult> {
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
private handleInitialize(_params: JSONRPCParams): InitializeResult {
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Remove async from handleListTools (line 70)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
private async handleListTools(): Promise<ListToolsResult> {
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
private handleListTools(): ListToolsResult {
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Update handleRequest callers (lines 41-43)**
|
||||||
|
|
||||||
|
Since handleInitialize and handleListTools are no longer async, remove the await:
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
case 'initialize':
|
||||||
|
return this.createSuccessResponse(request.id, await this.handleInitialize(request.params ?? {}));
|
||||||
|
case 'tools/list':
|
||||||
|
return this.createSuccessResponse(request.id, await this.handleListTools());
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
case 'initialize':
|
||||||
|
return this.createSuccessResponse(request.id, this.handleInitialize(request.params ?? {}));
|
||||||
|
case 'tools/list':
|
||||||
|
return this.createSuccessResponse(request.id, this.handleListTools());
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Remove eslint-disable and fix any type (lines 77-79)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tool arguments come from JSON-RPC and need runtime validation
|
||||||
|
const paramsObj = params as { name: string; arguments: any };
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
const paramsObj = params as { name: string; arguments: Record<string, unknown> };
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 5: Fix promise rejection to use Error (line 117)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
reject(error);
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
reject(error instanceof Error ? error : new Error(String(error)));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 6: Verify build**
|
||||||
|
|
||||||
|
Run: `npm run build`
|
||||||
|
Expected: No errors
|
||||||
|
|
||||||
|
**Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/server/mcp-server.ts
|
||||||
|
git commit -m "fix: async/await, eslint directive, and promise rejection in mcp-server.ts"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Fix routes.ts Promise Issue
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/server/routes.ts:10-19`
|
||||||
|
|
||||||
|
**Step 1: Wrap async handler to handle void context**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
app.post('/mcp', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const request = req.body as JSONRPCRequest;
|
||||||
|
const response = await handleRequest(request);
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MCP request error:', error);
|
||||||
|
res.status(500).json(createErrorResponse(null, ErrorCodes.InternalError, 'Internal server error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
app.post('/mcp', (req: Request, res: Response) => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const request = req.body as JSONRPCRequest;
|
||||||
|
const response = await handleRequest(request);
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MCP request error:', error);
|
||||||
|
res.status(500).json(createErrorResponse(null, ErrorCodes.InternalError, 'Internal server error'));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify build**
|
||||||
|
|
||||||
|
Run: `npm run build`
|
||||||
|
Expected: No errors
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/server/routes.ts
|
||||||
|
git commit -m "fix: wrap async handler with void for proper promise handling"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Fix tools/index.ts ESLint Directive
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/tools/index.ts:477-478`
|
||||||
|
|
||||||
|
**Step 1: Remove eslint-disable and fix type**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tool arguments come from JSON-RPC and require runtime validation
|
||||||
|
async callTool(name: string, args: any): Promise<CallToolResult> {
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
async callTool(name: string, args: Record<string, unknown>): Promise<CallToolResult> {
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify build**
|
||||||
|
|
||||||
|
Run: `npm run build`
|
||||||
|
Expected: No errors
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/tools/index.ts
|
||||||
|
git commit -m "fix: remove eslint-disable directive in tools/index.ts"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Fix vault-tools.ts Async Methods
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/tools/vault-tools.ts:18,63,310,498,925`
|
||||||
|
|
||||||
|
**Step 1: Remove async from getVaultInfo (line 18)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
async getVaultInfo(): Promise<CallToolResult> {
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
getVaultInfo(): CallToolResult {
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Remove async from listNotes (line 63)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
async listNotes(path?: string): Promise<CallToolResult> {
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
listNotes(path?: string): CallToolResult {
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Remove async from createFileMetadataWithFrontmatter (line 310)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
private async createFileMetadataWithFrontmatter(
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
private createFileMetadataWithFrontmatter(
|
||||||
|
```
|
||||||
|
|
||||||
|
Also update the return type from `Promise<FileMetadataWithFrontmatter>` to `FileMetadataWithFrontmatter`.
|
||||||
|
|
||||||
|
**Step 4: Remove async from exists (line 498)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
async exists(path: string): Promise<CallToolResult> {
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
exists(path: string): CallToolResult {
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 5: Remove async from resolveWikilink (line 925)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
async resolveWikilink(sourcePath: string, linkText: string): Promise<CallToolResult> {
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
resolveWikilink(sourcePath: string, linkText: string): CallToolResult {
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 6: Update callers if any use await on these methods**
|
||||||
|
|
||||||
|
Search for any `await this.getVaultInfo()`, `await this.listNotes()`, `await this.exists()`, `await this.resolveWikilink()`, `await this.createFileMetadataWithFrontmatter()` and remove the `await` keyword.
|
||||||
|
|
||||||
|
**Step 7: Verify build**
|
||||||
|
|
||||||
|
Run: `npm run build`
|
||||||
|
Expected: No errors
|
||||||
|
|
||||||
|
**Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/tools/vault-tools.ts
|
||||||
|
git commit -m "fix: remove async from methods without await in vault-tools.ts"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: Fix notifications.ts ESLint Directives
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ui/notifications.ts:10-11,78-79,145-146,179`
|
||||||
|
|
||||||
|
**Step 1: Fix interface args type (lines 10-11)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tool arguments come from JSON-RPC and can be any valid JSON structure
|
||||||
|
args: any;
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
args: Record<string, unknown>;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Fix showToolCall parameter type (lines 78-79)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tool arguments come from JSON-RPC and can be any valid JSON structure
|
||||||
|
showToolCall(toolName: string, args: any, duration?: number): void {
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
showToolCall(toolName: string, args: Record<string, unknown>, duration?: number): void {
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Fix formatArgs parameter type (lines 145-146)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tool arguments come from JSON-RPC and can be any valid JSON structure
|
||||||
|
private formatArgs(args: any): string {
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
private formatArgs(args: Record<string, unknown>): string {
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Fix unused 'e' variable (line 179)**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
} catch (e) {
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
} catch {
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 5: Verify build**
|
||||||
|
|
||||||
|
Run: `npm run build`
|
||||||
|
Expected: No errors
|
||||||
|
|
||||||
|
**Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ui/notifications.ts
|
||||||
|
git commit -m "fix: remove eslint directives and unused catch variable in notifications.ts"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8: Fix crypto-adapter.ts Require Import
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/utils/crypto-adapter.ts:18-34`
|
||||||
|
|
||||||
|
**Step 1: Replace require with dynamic approach**
|
||||||
|
|
||||||
|
The challenge here is that require() is used for synchronous access. We need to restructure to use a lazy initialization pattern.
|
||||||
|
|
||||||
|
Change the entire Node.js section from:
|
||||||
|
```typescript
|
||||||
|
// Node.js environment (15+) - uses Web Crypto API standard
|
||||||
|
if (typeof global !== 'undefined') {
|
||||||
|
try {
|
||||||
|
// Using require() is necessary for synchronous crypto access in Obsidian desktop plugins
|
||||||
|
// ES6 dynamic imports would create race conditions as crypto must be available synchronously
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires -- Synchronous Node.js crypto API access required
|
||||||
|
const nodeCrypto = require('crypto') as typeof import('crypto');
|
||||||
|
if (nodeCrypto?.webcrypto) {
|
||||||
|
return nodeCrypto.webcrypto as unknown as Crypto;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Crypto module not available or failed to load
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To (using globalThis.crypto which is available in Node 19+ and Electron):
|
||||||
|
```typescript
|
||||||
|
// Node.js/Electron environment - globalThis.crypto available in modern runtimes
|
||||||
|
if (typeof globalThis !== 'undefined' && globalThis.crypto) {
|
||||||
|
return globalThis.crypto;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify build**
|
||||||
|
|
||||||
|
Run: `npm run build`
|
||||||
|
Expected: No errors
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/utils/crypto-adapter.ts
|
||||||
|
git commit -m "fix: use globalThis.crypto instead of require('crypto')"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 9: Fix encryption-utils.ts Require Import
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/utils/encryption-utils.ts:8-18`
|
||||||
|
|
||||||
|
**Step 1: Restructure electron import**
|
||||||
|
|
||||||
|
Since Electron's safeStorage must be accessed synchronously at module load time, and ES6 dynamic imports are async, we need to use a different approach. In Obsidian plugins running in Electron, we can access electron through the window object.
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
// Safely import safeStorage - may not be available in all environments
|
||||||
|
let safeStorage: ElectronSafeStorage | null = null;
|
||||||
|
try {
|
||||||
|
// Using require() is necessary for synchronous access to Electron's safeStorage API in Obsidian desktop plugins
|
||||||
|
// ES6 dynamic imports would create race conditions as this module must be available synchronously
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires -- Synchronous Electron API access required for Obsidian plugin
|
||||||
|
const electron = require('electron') as typeof import('electron');
|
||||||
|
safeStorage = electron.safeStorage || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Electron safeStorage not available, API keys will be stored in plaintext');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
// Safely import safeStorage - may not be available in all environments
|
||||||
|
let safeStorage: ElectronSafeStorage | null = null;
|
||||||
|
try {
|
||||||
|
// Access electron through the global window object in Obsidian's Electron environment
|
||||||
|
// This avoids require() while still getting synchronous access
|
||||||
|
const electronRemote = (window as Window & { require?: (module: string) => typeof import('electron') }).require;
|
||||||
|
if (electronRemote) {
|
||||||
|
const electron = electronRemote('electron');
|
||||||
|
safeStorage = electron.safeStorage || null;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.warn('Electron safeStorage not available, API keys will be stored in plaintext');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify build**
|
||||||
|
|
||||||
|
Run: `npm run build`
|
||||||
|
Expected: No errors
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/utils/encryption-utils.ts
|
||||||
|
git commit -m "fix: use window.require pattern instead of bare require for electron"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 10: Fix link-utils.ts Async Method
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/utils/link-utils.ts:448`
|
||||||
|
|
||||||
|
**Step 1: Remove async from validateLinks**
|
||||||
|
|
||||||
|
Change from:
|
||||||
|
```typescript
|
||||||
|
static async validateLinks(
|
||||||
|
vault: IVaultAdapter,
|
||||||
|
metadata: IMetadataCacheAdapter,
|
||||||
|
content: string,
|
||||||
|
sourcePath: string
|
||||||
|
): Promise<LinkValidationResult> {
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```typescript
|
||||||
|
static validateLinks(
|
||||||
|
vault: IVaultAdapter,
|
||||||
|
metadata: IMetadataCacheAdapter,
|
||||||
|
content: string,
|
||||||
|
sourcePath: string
|
||||||
|
): LinkValidationResult {
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Update any callers that await this method**
|
||||||
|
|
||||||
|
Search for `await LinkUtils.validateLinks` or `await this.validateLinks` and remove the `await`.
|
||||||
|
|
||||||
|
**Step 3: Verify build**
|
||||||
|
|
||||||
|
Run: `npm run build`
|
||||||
|
Expected: No errors
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/utils/link-utils.ts
|
||||||
|
git commit -m "fix: remove async from validateLinks method"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 11: Final Build and Test
|
||||||
|
|
||||||
|
**Step 1: Run full build**
|
||||||
|
|
||||||
|
Run: `npm run build`
|
||||||
|
Expected: No errors
|
||||||
|
|
||||||
|
**Step 2: Run tests**
|
||||||
|
|
||||||
|
Run: `npm test`
|
||||||
|
Expected: All tests pass
|
||||||
|
|
||||||
|
**Step 3: Commit any remaining changes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status
|
||||||
|
# If any uncommitted changes:
|
||||||
|
git add -A
|
||||||
|
git commit -m "fix: final cleanup for code review issues"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optional Tasks (if time permits)
|
||||||
|
|
||||||
|
### Optional Task A: Fix Unused Error Variables
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/tools/vault-tools.ts:289,359,393,445,715`
|
||||||
|
- `src/utils/encryption-utils.ts:16`
|
||||||
|
- `src/utils/frontmatter-utils.ts:76,329,358`
|
||||||
|
- `src/utils/search-utils.ts:117,326`
|
||||||
|
- `src/utils/waypoint-utils.ts:103`
|
||||||
|
|
||||||
|
For each occurrence, change `catch (error) {` or `catch (e) {` or `catch (decompressError) {` to just `catch {`.
|
||||||
|
|
||||||
|
### Optional Task B: Use FileManager.trashFile()
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/adapters/vault-adapter.ts:46-48`
|
||||||
|
- Modify: `src/adapters/interfaces.ts` (update IVaultAdapter interface)
|
||||||
|
|
||||||
|
This requires passing the App or FileManager to the VaultAdapter, which is a larger refactor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary Checklist
|
||||||
|
|
||||||
|
- [ ] Task 1: main.ts sentence case + onunload
|
||||||
|
- [ ] Task 2: settings.ts sentence case
|
||||||
|
- [ ] Task 3: mcp-server.ts async/eslint/promise fixes
|
||||||
|
- [ ] Task 4: routes.ts promise handling
|
||||||
|
- [ ] Task 5: tools/index.ts eslint directive
|
||||||
|
- [ ] Task 6: vault-tools.ts async methods
|
||||||
|
- [ ] Task 7: notifications.ts eslint directives
|
||||||
|
- [ ] Task 8: crypto-adapter.ts require import
|
||||||
|
- [ ] Task 9: encryption-utils.ts require import
|
||||||
|
- [ ] Task 10: link-utils.ts async method
|
||||||
|
- [ ] Task 11: Final build and test
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "mcp-server",
|
"id": "mcp-server",
|
||||||
"name": "MCP Server",
|
"name": "MCP Server",
|
||||||
"version": "1.1.2",
|
"version": "1.1.3",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "Exposes vault operations via Model Context Protocol (MCP) over HTTP.",
|
"description": "Exposes vault operations via Model Context Protocol (MCP) over HTTP.",
|
||||||
"author": "William Ballou",
|
"author": "William Ballou",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mcp-server",
|
"name": "mcp-server",
|
||||||
"version": "1.1.2",
|
"version": "1.1.3",
|
||||||
"description": "MCP (Model Context Protocol) server plugin - exposes vault operations via HTTP",
|
"description": "MCP (Model Context Protocol) server plugin - exposes vault operations via HTTP",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default class MCPServerPlugin extends Plugin {
|
|||||||
this.updateStatusBar();
|
this.updateStatusBar();
|
||||||
|
|
||||||
// Add ribbon icon to toggle server
|
// Add ribbon icon to toggle server
|
||||||
this.addRibbonIcon('server', 'Toggle MCP Server', async () => {
|
this.addRibbonIcon('server', 'Toggle MCP server', async () => {
|
||||||
if (this.mcpServer?.isRunning()) {
|
if (this.mcpServer?.isRunning()) {
|
||||||
await this.stopServer();
|
await this.stopServer();
|
||||||
} else {
|
} else {
|
||||||
@@ -93,8 +93,8 @@ export default class MCPServerPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onunload() {
|
onunload() {
|
||||||
await this.stopServer();
|
void this.stopServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async startServer() {
|
async startServer() {
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ export class MCPServer {
|
|||||||
try {
|
try {
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case 'initialize':
|
case 'initialize':
|
||||||
return this.createSuccessResponse(request.id, await this.handleInitialize(request.params ?? {}));
|
return this.createSuccessResponse(request.id, this.handleInitialize(request.params ?? {}));
|
||||||
case 'tools/list':
|
case 'tools/list':
|
||||||
return this.createSuccessResponse(request.id, await this.handleListTools());
|
return this.createSuccessResponse(request.id, this.handleListTools());
|
||||||
case 'tools/call':
|
case 'tools/call':
|
||||||
return this.createSuccessResponse(request.id, await this.handleCallTool(request.params ?? {}));
|
return this.createSuccessResponse(request.id, await this.handleCallTool(request.params ?? {}));
|
||||||
case 'ping':
|
case 'ping':
|
||||||
@@ -54,7 +54,7 @@ export class MCPServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleInitialize(_params: JSONRPCParams): Promise<InitializeResult> {
|
private handleInitialize(_params: JSONRPCParams): InitializeResult {
|
||||||
return {
|
return {
|
||||||
protocolVersion: "2024-11-05",
|
protocolVersion: "2024-11-05",
|
||||||
capabilities: {
|
capabilities: {
|
||||||
@@ -67,15 +67,14 @@ export class MCPServer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleListTools(): Promise<ListToolsResult> {
|
private handleListTools(): ListToolsResult {
|
||||||
return {
|
return {
|
||||||
tools: this.toolRegistry.getToolDefinitions()
|
tools: this.toolRegistry.getToolDefinitions()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleCallTool(params: JSONRPCParams): Promise<CallToolResult> {
|
private async handleCallTool(params: JSONRPCParams): Promise<CallToolResult> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tool arguments come from JSON-RPC and need runtime validation
|
const paramsObj = params as { name: string; arguments: Record<string, unknown> };
|
||||||
const paramsObj = params as { name: string; arguments: any };
|
|
||||||
return await this.toolRegistry.callTool(paramsObj.name, paramsObj.arguments);
|
return await this.toolRegistry.callTool(paramsObj.name, paramsObj.arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +113,7 @@ export class MCPServer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
reject(error instanceof Error ? error : new Error(String(error)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,17 @@ export function setupRoutes(
|
|||||||
createErrorResponse: (id: string | number | null, code: number, message: string) => JSONRPCResponse
|
createErrorResponse: (id: string | number | null, code: number, message: string) => JSONRPCResponse
|
||||||
): void {
|
): void {
|
||||||
// Main MCP endpoint
|
// Main MCP endpoint
|
||||||
app.post('/mcp', async (req: Request, res: Response) => {
|
app.post('/mcp', (req: Request, res: Response) => {
|
||||||
try {
|
void (async () => {
|
||||||
const request = req.body as JSONRPCRequest;
|
try {
|
||||||
const response = await handleRequest(request);
|
const request = req.body as JSONRPCRequest;
|
||||||
res.json(response);
|
const response = await handleRequest(request);
|
||||||
} catch (error) {
|
res.json(response);
|
||||||
console.error('MCP request error:', error);
|
} catch (error) {
|
||||||
res.status(500).json(createErrorResponse(null, ErrorCodes.InternalError, 'Internal server error'));
|
console.error('MCP request error:', error);
|
||||||
}
|
res.status(500).json(createErrorResponse(null, ErrorCodes.InternalError, 'Internal server error'));
|
||||||
|
}
|
||||||
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ export class MCPServerSettingTab extends PluginSettingTab {
|
|||||||
// Authentication (Always Enabled)
|
// Authentication (Always Enabled)
|
||||||
const authDetails = containerEl.createEl('details', {cls: 'mcp-auth-section'});
|
const authDetails = containerEl.createEl('details', {cls: 'mcp-auth-section'});
|
||||||
const authSummary = authDetails.createEl('summary', {cls: 'mcp-auth-summary'});
|
const authSummary = authDetails.createEl('summary', {cls: 'mcp-auth-summary'});
|
||||||
authSummary.setText('Authentication & Configuration');
|
authSummary.setText('Authentication & configuration');
|
||||||
|
|
||||||
// Store reference for targeted updates
|
// Store reference for targeted updates
|
||||||
this.authDetailsEl = authDetails;
|
this.authDetailsEl = authDetails;
|
||||||
@@ -316,7 +316,7 @@ export class MCPServerSettingTab extends PluginSettingTab {
|
|||||||
// Notification Settings
|
// Notification Settings
|
||||||
const notifDetails = containerEl.createEl('details', {cls: 'mcp-auth-section'});
|
const notifDetails = containerEl.createEl('details', {cls: 'mcp-auth-section'});
|
||||||
const notifSummary = notifDetails.createEl('summary', {cls: 'mcp-auth-summary'});
|
const notifSummary = notifDetails.createEl('summary', {cls: 'mcp-auth-summary'});
|
||||||
notifSummary.setText('UI Notifications');
|
notifSummary.setText('UI notifications');
|
||||||
|
|
||||||
// Store reference for targeted updates
|
// Store reference for targeted updates
|
||||||
this.notificationDetailsEl = notifDetails;
|
this.notificationDetailsEl = notifDetails;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { VaultTools } from './vault-tools';
|
|||||||
import { createNoteTools } from './note-tools-factory';
|
import { createNoteTools } from './note-tools-factory';
|
||||||
import { createVaultTools } from './vault-tools-factory';
|
import { createVaultTools } from './vault-tools-factory';
|
||||||
import { NotificationManager } from '../ui/notifications';
|
import { NotificationManager } from '../ui/notifications';
|
||||||
|
import { YAMLValue } from '../utils/frontmatter-utils';
|
||||||
|
|
||||||
export class ToolRegistry {
|
export class ToolRegistry {
|
||||||
private noteTools: NoteTools;
|
private noteTools: NoteTools;
|
||||||
@@ -474,8 +475,7 @@ export class ToolRegistry {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tool arguments come from JSON-RPC and require runtime validation
|
async callTool(name: string, args: Record<string, unknown>): Promise<CallToolResult> {
|
||||||
async callTool(name: string, args: any): Promise<CallToolResult> {
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
// Show tool call notification
|
// Show tool call notification
|
||||||
@@ -487,124 +487,160 @@ export class ToolRegistry {
|
|||||||
let result: CallToolResult;
|
let result: CallToolResult;
|
||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "read_note":
|
case "read_note": {
|
||||||
result = await this.noteTools.readNote(args.path, {
|
const a = args as { path: string; withFrontmatter?: boolean; withContent?: boolean; parseFrontmatter?: boolean };
|
||||||
withFrontmatter: args.withFrontmatter,
|
result = await this.noteTools.readNote(a.path, {
|
||||||
withContent: args.withContent,
|
withFrontmatter: a.withFrontmatter,
|
||||||
parseFrontmatter: args.parseFrontmatter
|
withContent: a.withContent,
|
||||||
|
parseFrontmatter: a.parseFrontmatter
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "create_note":
|
}
|
||||||
|
case "create_note": {
|
||||||
|
const a = args as { path: string; content: string; createParents?: boolean; onConflict?: 'error' | 'overwrite' | 'rename'; validateLinks?: boolean };
|
||||||
result = await this.noteTools.createNote(
|
result = await this.noteTools.createNote(
|
||||||
args.path,
|
a.path,
|
||||||
args.content,
|
a.content,
|
||||||
args.createParents ?? false,
|
a.createParents ?? false,
|
||||||
args.onConflict ?? 'error',
|
a.onConflict ?? 'error',
|
||||||
args.validateLinks ?? true
|
a.validateLinks ?? true
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "update_note":
|
}
|
||||||
|
case "update_note": {
|
||||||
|
const a = args as { path: string; content: string; validateLinks?: boolean };
|
||||||
result = await this.noteTools.updateNote(
|
result = await this.noteTools.updateNote(
|
||||||
args.path,
|
a.path,
|
||||||
args.content,
|
a.content,
|
||||||
args.validateLinks ?? true
|
a.validateLinks ?? true
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "update_frontmatter":
|
}
|
||||||
|
case "update_frontmatter": {
|
||||||
|
const a = args as { path: string; patch?: Record<string, YAMLValue>; remove?: string[]; ifMatch?: string };
|
||||||
result = await this.noteTools.updateFrontmatter(
|
result = await this.noteTools.updateFrontmatter(
|
||||||
args.path,
|
a.path,
|
||||||
args.patch,
|
a.patch,
|
||||||
args.remove ?? [],
|
a.remove ?? [],
|
||||||
args.ifMatch
|
a.ifMatch
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "update_sections":
|
}
|
||||||
|
case "update_sections": {
|
||||||
|
const a = args as { path: string; edits: Array<{ startLine: number; endLine: number; content: string }>; ifMatch?: string; validateLinks?: boolean };
|
||||||
result = await this.noteTools.updateSections(
|
result = await this.noteTools.updateSections(
|
||||||
args.path,
|
a.path,
|
||||||
args.edits,
|
a.edits,
|
||||||
args.ifMatch,
|
a.ifMatch,
|
||||||
args.validateLinks ?? true
|
a.validateLinks ?? true
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "rename_file":
|
}
|
||||||
|
case "rename_file": {
|
||||||
|
const a = args as { path: string; newPath: string; updateLinks?: boolean; ifMatch?: string };
|
||||||
result = await this.noteTools.renameFile(
|
result = await this.noteTools.renameFile(
|
||||||
args.path,
|
a.path,
|
||||||
args.newPath,
|
a.newPath,
|
||||||
args.updateLinks ?? true,
|
a.updateLinks ?? true,
|
||||||
args.ifMatch
|
a.ifMatch
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "delete_note":
|
}
|
||||||
|
case "delete_note": {
|
||||||
|
const a = args as { path: string; soft?: boolean; dryRun?: boolean; ifMatch?: string };
|
||||||
result = await this.noteTools.deleteNote(
|
result = await this.noteTools.deleteNote(
|
||||||
args.path,
|
a.path,
|
||||||
args.soft ?? true,
|
a.soft ?? true,
|
||||||
args.dryRun ?? false,
|
a.dryRun ?? false,
|
||||||
args.ifMatch
|
a.ifMatch
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "search":
|
}
|
||||||
|
case "search": {
|
||||||
|
const a = args as { query: string; isRegex?: boolean; caseSensitive?: boolean; includes?: string[]; excludes?: string[]; folder?: string; returnSnippets?: boolean; snippetLength?: number; maxResults?: number };
|
||||||
result = await this.vaultTools.search({
|
result = await this.vaultTools.search({
|
||||||
query: args.query,
|
query: a.query,
|
||||||
isRegex: args.isRegex,
|
isRegex: a.isRegex,
|
||||||
caseSensitive: args.caseSensitive,
|
caseSensitive: a.caseSensitive,
|
||||||
includes: args.includes,
|
includes: a.includes,
|
||||||
excludes: args.excludes,
|
excludes: a.excludes,
|
||||||
folder: args.folder,
|
folder: a.folder,
|
||||||
returnSnippets: args.returnSnippets,
|
returnSnippets: a.returnSnippets,
|
||||||
snippetLength: args.snippetLength,
|
snippetLength: a.snippetLength,
|
||||||
maxResults: args.maxResults
|
maxResults: a.maxResults
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "search_waypoints":
|
}
|
||||||
result = await this.vaultTools.searchWaypoints(args.folder);
|
case "search_waypoints": {
|
||||||
|
const a = args as { folder?: string };
|
||||||
|
result = await this.vaultTools.searchWaypoints(a.folder);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "get_vault_info":
|
case "get_vault_info":
|
||||||
result = await this.vaultTools.getVaultInfo();
|
result = this.vaultTools.getVaultInfo();
|
||||||
break;
|
break;
|
||||||
case "list":
|
case "list": {
|
||||||
|
const a = args as { path?: string; recursive?: boolean; includes?: string[]; excludes?: string[]; only?: 'files' | 'directories' | 'any'; limit?: number; cursor?: string; withFrontmatterSummary?: boolean; includeWordCount?: boolean };
|
||||||
result = await this.vaultTools.list({
|
result = await this.vaultTools.list({
|
||||||
path: args.path,
|
path: a.path,
|
||||||
recursive: args.recursive,
|
recursive: a.recursive,
|
||||||
includes: args.includes,
|
includes: a.includes,
|
||||||
excludes: args.excludes,
|
excludes: a.excludes,
|
||||||
only: args.only,
|
only: a.only,
|
||||||
limit: args.limit,
|
limit: a.limit,
|
||||||
cursor: args.cursor,
|
cursor: a.cursor,
|
||||||
withFrontmatterSummary: args.withFrontmatterSummary,
|
withFrontmatterSummary: a.withFrontmatterSummary,
|
||||||
includeWordCount: args.includeWordCount
|
includeWordCount: a.includeWordCount
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "stat":
|
}
|
||||||
result = await this.vaultTools.stat(args.path, args.includeWordCount);
|
case "stat": {
|
||||||
|
const a = args as { path: string; includeWordCount?: boolean };
|
||||||
|
result = await this.vaultTools.stat(a.path, a.includeWordCount);
|
||||||
break;
|
break;
|
||||||
case "exists":
|
}
|
||||||
result = await this.vaultTools.exists(args.path);
|
case "exists": {
|
||||||
|
const a = args as { path: string };
|
||||||
|
result = this.vaultTools.exists(a.path);
|
||||||
break;
|
break;
|
||||||
case "read_excalidraw":
|
}
|
||||||
result = await this.noteTools.readExcalidraw(args.path, {
|
case "read_excalidraw": {
|
||||||
includeCompressed: args.includeCompressed,
|
const a = args as { path: string; includeCompressed?: boolean; includePreview?: boolean };
|
||||||
includePreview: args.includePreview
|
result = await this.noteTools.readExcalidraw(a.path, {
|
||||||
|
includeCompressed: a.includeCompressed,
|
||||||
|
includePreview: a.includePreview
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "get_folder_waypoint":
|
}
|
||||||
result = await this.vaultTools.getFolderWaypoint(args.path);
|
case "get_folder_waypoint": {
|
||||||
|
const a = args as { path: string };
|
||||||
|
result = await this.vaultTools.getFolderWaypoint(a.path);
|
||||||
break;
|
break;
|
||||||
case "is_folder_note":
|
}
|
||||||
result = await this.vaultTools.isFolderNote(args.path);
|
case "is_folder_note": {
|
||||||
|
const a = args as { path: string };
|
||||||
|
result = await this.vaultTools.isFolderNote(a.path);
|
||||||
break;
|
break;
|
||||||
case "validate_wikilinks":
|
}
|
||||||
result = await this.vaultTools.validateWikilinks(args.path);
|
case "validate_wikilinks": {
|
||||||
|
const a = args as { path: string };
|
||||||
|
result = await this.vaultTools.validateWikilinks(a.path);
|
||||||
break;
|
break;
|
||||||
case "resolve_wikilink":
|
}
|
||||||
result = await this.vaultTools.resolveWikilink(args.sourcePath, args.linkText);
|
case "resolve_wikilink": {
|
||||||
|
const a = args as { sourcePath: string; linkText: string };
|
||||||
|
result = this.vaultTools.resolveWikilink(a.sourcePath, a.linkText);
|
||||||
break;
|
break;
|
||||||
case "backlinks":
|
}
|
||||||
|
case "backlinks": {
|
||||||
|
const a = args as { path: string; includeUnlinked?: boolean; includeSnippets?: boolean };
|
||||||
result = await this.vaultTools.getBacklinks(
|
result = await this.vaultTools.getBacklinks(
|
||||||
args.path,
|
a.path,
|
||||||
args.includeUnlinked ?? false,
|
a.includeUnlinked ?? false,
|
||||||
args.includeSnippets ?? true
|
a.includeSnippets ?? true
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
result = {
|
result = {
|
||||||
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ export class NoteTools {
|
|||||||
|
|
||||||
// Add link validation if requested
|
// Add link validation if requested
|
||||||
if (validateLinks) {
|
if (validateLinks) {
|
||||||
result.linkValidation = await LinkUtils.validateLinks(
|
result.linkValidation = LinkUtils.validateLinks(
|
||||||
this.vault,
|
this.vault,
|
||||||
this.metadata,
|
this.metadata,
|
||||||
content,
|
content,
|
||||||
@@ -388,7 +388,7 @@ export class NoteTools {
|
|||||||
|
|
||||||
// Add link validation if requested
|
// Add link validation if requested
|
||||||
if (validateLinks) {
|
if (validateLinks) {
|
||||||
result.linkValidation = await LinkUtils.validateLinks(
|
result.linkValidation = LinkUtils.validateLinks(
|
||||||
this.vault,
|
this.vault,
|
||||||
this.metadata,
|
this.metadata,
|
||||||
content,
|
content,
|
||||||
@@ -990,7 +990,7 @@ export class NoteTools {
|
|||||||
|
|
||||||
// Add link validation if requested
|
// Add link validation if requested
|
||||||
if (validateLinks) {
|
if (validateLinks) {
|
||||||
result.linkValidation = await LinkUtils.validateLinks(
|
result.linkValidation = LinkUtils.validateLinks(
|
||||||
this.vault,
|
this.vault,
|
||||||
this.metadata,
|
this.metadata,
|
||||||
newContent,
|
newContent,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export class VaultTools {
|
|||||||
private metadata: IMetadataCacheAdapter
|
private metadata: IMetadataCacheAdapter
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getVaultInfo(): Promise<CallToolResult> {
|
getVaultInfo(): CallToolResult {
|
||||||
try {
|
try {
|
||||||
const allFiles = this.vault.getMarkdownFiles();
|
const allFiles = this.vault.getMarkdownFiles();
|
||||||
const totalNotes = allFiles.length;
|
const totalNotes = allFiles.length;
|
||||||
@@ -60,7 +60,7 @@ export class VaultTools {
|
|||||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
async listNotes(path?: string): Promise<CallToolResult> {
|
listNotes(path?: string): CallToolResult {
|
||||||
let items: Array<FileMetadata | DirectoryMetadata> = [];
|
let items: Array<FileMetadata | DirectoryMetadata> = [];
|
||||||
|
|
||||||
// Normalize root path: undefined, empty string "", or "." all mean root
|
// Normalize root path: undefined, empty string "", or "." all mean root
|
||||||
@@ -279,7 +279,7 @@ export class VaultTools {
|
|||||||
// Apply type filtering and add items
|
// Apply type filtering and add items
|
||||||
if (item instanceof TFile) {
|
if (item instanceof TFile) {
|
||||||
if (only !== 'directories') {
|
if (only !== 'directories') {
|
||||||
const fileMetadata = await this.createFileMetadataWithFrontmatter(item, withFrontmatterSummary || false);
|
const fileMetadata = this.createFileMetadataWithFrontmatter(item, withFrontmatterSummary || false);
|
||||||
|
|
||||||
// Optionally include word count (best effort)
|
// Optionally include word count (best effort)
|
||||||
if (includeWordCount) {
|
if (includeWordCount) {
|
||||||
@@ -307,10 +307,10 @@ export class VaultTools {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createFileMetadataWithFrontmatter(
|
private createFileMetadataWithFrontmatter(
|
||||||
file: TFile,
|
file: TFile,
|
||||||
withFrontmatterSummary: boolean
|
withFrontmatterSummary: boolean
|
||||||
): Promise<FileMetadataWithFrontmatter> {
|
): FileMetadataWithFrontmatter {
|
||||||
const baseMetadata = this.createFileMetadata(file);
|
const baseMetadata = this.createFileMetadata(file);
|
||||||
|
|
||||||
if (!withFrontmatterSummary || file.extension !== 'md') {
|
if (!withFrontmatterSummary || file.extension !== 'md') {
|
||||||
@@ -495,7 +495,7 @@ export class VaultTools {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async exists(path: string): Promise<CallToolResult> {
|
exists(path: string): CallToolResult {
|
||||||
// Validate path
|
// Validate path
|
||||||
if (!PathUtils.isValidVaultPath(path)) {
|
if (!PathUtils.isValidVaultPath(path)) {
|
||||||
return {
|
return {
|
||||||
@@ -922,7 +922,7 @@ export class VaultTools {
|
|||||||
* Resolve a single wikilink from a source note
|
* Resolve a single wikilink from a source note
|
||||||
* Returns the target path if resolvable, or suggestions if not
|
* Returns the target path if resolvable, or suggestions if not
|
||||||
*/
|
*/
|
||||||
async resolveWikilink(sourcePath: string, linkText: string): Promise<CallToolResult> {
|
resolveWikilink(sourcePath: string, linkText: string): CallToolResult {
|
||||||
try {
|
try {
|
||||||
// Normalize and validate source path
|
// Normalize and validate source path
|
||||||
const normalizedPath = PathUtils.normalizePath(sourcePath);
|
const normalizedPath = PathUtils.normalizePath(sourcePath);
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { MCPPluginSettings } from '../types/settings-types';
|
|||||||
export interface NotificationHistoryEntry {
|
export interface NotificationHistoryEntry {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
toolName: string;
|
toolName: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tool arguments come from JSON-RPC and can be any valid JSON structure
|
args: Record<string, unknown>;
|
||||||
args: any;
|
|
||||||
success: boolean;
|
success: boolean;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -75,8 +74,7 @@ export class NotificationManager {
|
|||||||
/**
|
/**
|
||||||
* Show notification for tool call start
|
* Show notification for tool call start
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tool arguments come from JSON-RPC and can be any valid JSON structure
|
showToolCall(toolName: string, args: Record<string, unknown>, duration?: number): void {
|
||||||
showToolCall(toolName: string, args: any, duration?: number): void {
|
|
||||||
if (!this.shouldShowNotification()) {
|
if (!this.shouldShowNotification()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -142,8 +140,7 @@ export class NotificationManager {
|
|||||||
/**
|
/**
|
||||||
* Format arguments for display
|
* Format arguments for display
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tool arguments come from JSON-RPC and can be any valid JSON structure
|
private formatArgs(args: Record<string, unknown>): string {
|
||||||
private formatArgs(args: any): string {
|
|
||||||
if (!this.settings.showParameters) {
|
if (!this.settings.showParameters) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -156,13 +153,13 @@ export class NotificationManager {
|
|||||||
// Extract key parameters for display
|
// Extract key parameters for display
|
||||||
const keyParams: string[] = [];
|
const keyParams: string[] = [];
|
||||||
|
|
||||||
if (args.path) {
|
if (args.path && typeof args.path === 'string') {
|
||||||
keyParams.push(`path: "${this.truncateString(args.path, 30)}"`);
|
keyParams.push(`path: "${this.truncateString(args.path, 30)}"`);
|
||||||
}
|
}
|
||||||
if (args.query) {
|
if (args.query && typeof args.query === 'string') {
|
||||||
keyParams.push(`query: "${this.truncateString(args.query, 30)}"`);
|
keyParams.push(`query: "${this.truncateString(args.query, 30)}"`);
|
||||||
}
|
}
|
||||||
if (args.folder) {
|
if (args.folder && typeof args.folder === 'string') {
|
||||||
keyParams.push(`folder: "${this.truncateString(args.folder, 30)}"`);
|
keyParams.push(`folder: "${this.truncateString(args.folder, 30)}"`);
|
||||||
}
|
}
|
||||||
if (args.recursive !== undefined) {
|
if (args.recursive !== undefined) {
|
||||||
@@ -176,7 +173,7 @@ export class NotificationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return keyParams.join(', ');
|
return keyParams.join(', ');
|
||||||
} catch (e) {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,19 +15,9 @@ function getCrypto(): Crypto {
|
|||||||
return window.crypto;
|
return window.crypto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node.js environment (15+) - uses Web Crypto API standard
|
// Node.js/Electron environment - globalThis.crypto available in Node 20+
|
||||||
if (typeof global !== 'undefined') {
|
if (typeof globalThis !== 'undefined' && globalThis.crypto) {
|
||||||
try {
|
return globalThis.crypto;
|
||||||
// Using require() is necessary for synchronous crypto access in Obsidian desktop plugins
|
|
||||||
// ES6 dynamic imports would create race conditions as crypto must be available synchronously
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires -- Synchronous Node.js crypto API access required
|
|
||||||
const nodeCrypto = require('crypto') as typeof import('crypto');
|
|
||||||
if (nodeCrypto?.webcrypto) {
|
|
||||||
return nodeCrypto.webcrypto as unknown as Crypto;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Crypto module not available or failed to load
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('No Web Crypto API available in this environment');
|
throw new Error('No Web Crypto API available in this environment');
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ interface ElectronSafeStorage {
|
|||||||
// Safely import safeStorage - may not be available in all environments
|
// Safely import safeStorage - may not be available in all environments
|
||||||
let safeStorage: ElectronSafeStorage | null = null;
|
let safeStorage: ElectronSafeStorage | null = null;
|
||||||
try {
|
try {
|
||||||
// Using require() is necessary for synchronous access to Electron's safeStorage API in Obsidian desktop plugins
|
// Access electron through the global window object in Obsidian's Electron environment
|
||||||
// ES6 dynamic imports would create race conditions as this module must be available synchronously
|
// This avoids require() while still getting synchronous access
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires -- Synchronous Electron API access required for Obsidian plugin
|
const electronRemote = (window as Window & { require?: (module: string) => typeof import('electron') }).require;
|
||||||
const electron = require('electron') as typeof import('electron');
|
if (electronRemote) {
|
||||||
safeStorage = electron.safeStorage || null;
|
const electron = electronRemote('electron');
|
||||||
} catch (error) {
|
safeStorage = electron.safeStorage || null;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
console.warn('Electron safeStorage not available, API keys will be stored in plaintext');
|
console.warn('Electron safeStorage not available, API keys will be stored in plaintext');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -445,12 +445,12 @@ export class LinkUtils {
|
|||||||
* @param sourcePath Path of the file containing the links
|
* @param sourcePath Path of the file containing the links
|
||||||
* @returns Structured validation result with categorized links
|
* @returns Structured validation result with categorized links
|
||||||
*/
|
*/
|
||||||
static async validateLinks(
|
static validateLinks(
|
||||||
vault: IVaultAdapter,
|
vault: IVaultAdapter,
|
||||||
metadata: IMetadataCacheAdapter,
|
metadata: IMetadataCacheAdapter,
|
||||||
content: string,
|
content: string,
|
||||||
sourcePath: string
|
sourcePath: string
|
||||||
): Promise<LinkValidationResult> {
|
): LinkValidationResult {
|
||||||
const valid: string[] = [];
|
const valid: string[] = [];
|
||||||
const brokenNotes: BrokenNoteLink[] = [];
|
const brokenNotes: BrokenNoteLink[] = [];
|
||||||
const brokenHeadings: BrokenHeadingLink[] = [];
|
const brokenHeadings: BrokenHeadingLink[] = [];
|
||||||
|
|||||||
@@ -138,11 +138,18 @@ describe('crypto-adapter', () => {
|
|||||||
const globalRef = global as any;
|
const globalRef = global as any;
|
||||||
const originalWindow = globalRef.window;
|
const originalWindow = globalRef.window;
|
||||||
const originalGlobal = globalRef.global;
|
const originalGlobal = globalRef.global;
|
||||||
|
const originalGlobalThisCrypto = globalThis.crypto;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Remove window.crypto and global access
|
// Remove window.crypto, global access, and globalThis.crypto
|
||||||
delete globalRef.window;
|
delete globalRef.window;
|
||||||
delete globalRef.global;
|
delete globalRef.global;
|
||||||
|
// In modern Node.js, globalThis.crypto is always available, so we must mock it too
|
||||||
|
Object.defineProperty(globalThis, 'crypto', {
|
||||||
|
value: undefined,
|
||||||
|
writable: true,
|
||||||
|
configurable: true
|
||||||
|
});
|
||||||
|
|
||||||
// Clear module cache to force re-evaluation
|
// Clear module cache to force re-evaluation
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
@@ -157,6 +164,12 @@ describe('crypto-adapter', () => {
|
|||||||
// Restore original values
|
// Restore original values
|
||||||
globalRef.window = originalWindow;
|
globalRef.window = originalWindow;
|
||||||
globalRef.global = originalGlobal;
|
globalRef.global = originalGlobal;
|
||||||
|
// Restore globalThis.crypto
|
||||||
|
Object.defineProperty(globalThis, 'crypto', {
|
||||||
|
value: originalGlobalThisCrypto,
|
||||||
|
writable: true,
|
||||||
|
configurable: true
|
||||||
|
});
|
||||||
|
|
||||||
// Clear module cache again to restore normal state
|
// Clear module cache again to restore normal state
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
|
|||||||
@@ -1,18 +1,63 @@
|
|||||||
import { encryptApiKey, decryptApiKey, isEncryptionAvailable } from '../src/utils/encryption-utils';
|
// Mock safeStorage implementation
|
||||||
|
const mockSafeStorage = {
|
||||||
|
isEncryptionAvailable: jest.fn(() => true),
|
||||||
|
encryptString: jest.fn((data: string) => Buffer.from(`encrypted:${data}`)),
|
||||||
|
decryptString: jest.fn((buffer: Buffer) => buffer.toString().replace('encrypted:', ''))
|
||||||
|
};
|
||||||
|
|
||||||
// Mock electron module
|
// Setup window.require mock before importing the module
|
||||||
jest.mock('electron', () => ({
|
const mockWindowRequire = jest.fn((module: string) => {
|
||||||
safeStorage: {
|
if (module === 'electron') {
|
||||||
isEncryptionAvailable: jest.fn(() => true),
|
return { safeStorage: mockSafeStorage };
|
||||||
encryptString: jest.fn((data: string) => Buffer.from(`encrypted:${data}`)),
|
|
||||||
decryptString: jest.fn((buffer: Buffer) => {
|
|
||||||
const str = buffer.toString();
|
|
||||||
return str.replace('encrypted:', '');
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}));
|
throw new Error(`Module not found: ${module}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create mock window object for Node environment
|
||||||
|
const mockWindow: Window & { require?: unknown } = {
|
||||||
|
require: mockWindowRequire
|
||||||
|
} as unknown as Window & { require?: unknown };
|
||||||
|
|
||||||
|
// Store original global window
|
||||||
|
const originalWindow = (globalThis as unknown as { window?: unknown }).window;
|
||||||
|
|
||||||
|
// Set up window.require before tests run
|
||||||
|
beforeAll(() => {
|
||||||
|
(globalThis as unknown as { window: typeof mockWindow }).window = mockWindow;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up after all tests
|
||||||
|
afterAll(() => {
|
||||||
|
if (originalWindow === undefined) {
|
||||||
|
delete (globalThis as unknown as { window?: unknown }).window;
|
||||||
|
} else {
|
||||||
|
(globalThis as unknown as { window: typeof originalWindow }).window = originalWindow;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Import after mock is set up - use require to ensure module loads after mock
|
||||||
|
let encryptApiKey: typeof import('../src/utils/encryption-utils').encryptApiKey;
|
||||||
|
let decryptApiKey: typeof import('../src/utils/encryption-utils').decryptApiKey;
|
||||||
|
let isEncryptionAvailable: typeof import('../src/utils/encryption-utils').isEncryptionAvailable;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
// Reset modules to ensure fresh load with mock
|
||||||
|
jest.resetModules();
|
||||||
|
const encryptionUtils = require('../src/utils/encryption-utils');
|
||||||
|
encryptApiKey = encryptionUtils.encryptApiKey;
|
||||||
|
decryptApiKey = encryptionUtils.decryptApiKey;
|
||||||
|
isEncryptionAvailable = encryptionUtils.isEncryptionAvailable;
|
||||||
|
});
|
||||||
|
|
||||||
describe('Encryption Utils', () => {
|
describe('Encryption Utils', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset mock implementations before each test
|
||||||
|
mockSafeStorage.isEncryptionAvailable.mockReturnValue(true);
|
||||||
|
mockSafeStorage.encryptString.mockImplementation((data: string) => Buffer.from(`encrypted:${data}`));
|
||||||
|
mockSafeStorage.decryptString.mockImplementation((buffer: Buffer) => buffer.toString().replace('encrypted:', ''));
|
||||||
|
mockWindowRequire.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
describe('encryptApiKey', () => {
|
describe('encryptApiKey', () => {
|
||||||
it('should encrypt API key when encryption is available', () => {
|
it('should encrypt API key when encryption is available', () => {
|
||||||
const apiKey = 'test-api-key-12345';
|
const apiKey = 'test-api-key-12345';
|
||||||
@@ -23,13 +68,23 @@ describe('Encryption Utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return plaintext when encryption is not available', () => {
|
it('should return plaintext when encryption is not available', () => {
|
||||||
const { safeStorage } = require('electron');
|
// Need to reload module with different mock behavior
|
||||||
safeStorage.isEncryptionAvailable.mockReturnValueOnce(false);
|
jest.resetModules();
|
||||||
|
const mockStorage = {
|
||||||
|
isEncryptionAvailable: jest.fn(() => false),
|
||||||
|
encryptString: jest.fn(),
|
||||||
|
decryptString: jest.fn()
|
||||||
|
};
|
||||||
|
mockWindow.require = jest.fn(() => ({ safeStorage: mockStorage }));
|
||||||
|
|
||||||
|
const { encryptApiKey: encrypt } = require('../src/utils/encryption-utils');
|
||||||
const apiKey = 'test-api-key-12345';
|
const apiKey = 'test-api-key-12345';
|
||||||
const result = encryptApiKey(apiKey);
|
const result = encrypt(apiKey);
|
||||||
|
|
||||||
expect(result).toBe(apiKey);
|
expect(result).toBe(apiKey);
|
||||||
|
|
||||||
|
// Restore original mock
|
||||||
|
mockWindow.require = mockWindowRequire;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty string', () => {
|
it('should handle empty string', () => {
|
||||||
@@ -73,92 +128,107 @@ describe('Encryption Utils', () => {
|
|||||||
|
|
||||||
describe('error handling', () => {
|
describe('error handling', () => {
|
||||||
it('should handle encryption errors and fallback to plaintext', () => {
|
it('should handle encryption errors and fallback to plaintext', () => {
|
||||||
const { safeStorage } = require('electron');
|
// Reload module with error-throwing mock
|
||||||
const originalEncrypt = safeStorage.encryptString;
|
jest.resetModules();
|
||||||
safeStorage.encryptString = jest.fn(() => {
|
const mockStorage = {
|
||||||
throw new Error('Encryption failed');
|
isEncryptionAvailable: jest.fn(() => true),
|
||||||
});
|
encryptString: jest.fn(() => {
|
||||||
|
throw new Error('Encryption failed');
|
||||||
|
}),
|
||||||
|
decryptString: jest.fn()
|
||||||
|
};
|
||||||
|
mockWindow.require = jest.fn(() => ({ safeStorage: mockStorage }));
|
||||||
|
|
||||||
|
const { encryptApiKey: encrypt } = require('../src/utils/encryption-utils');
|
||||||
const apiKey = 'test-api-key-12345';
|
const apiKey = 'test-api-key-12345';
|
||||||
const result = encryptApiKey(apiKey);
|
const result = encrypt(apiKey);
|
||||||
|
|
||||||
expect(result).toBe(apiKey); // Should return plaintext on error
|
expect(result).toBe(apiKey); // Should return plaintext on error
|
||||||
safeStorage.encryptString = originalEncrypt; // Restore
|
|
||||||
|
// Restore original mock
|
||||||
|
mockWindow.require = mockWindowRequire;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error when decryption fails', () => {
|
it('should throw error when decryption fails', () => {
|
||||||
const { safeStorage } = require('electron');
|
// Reload module with error-throwing mock
|
||||||
const originalDecrypt = safeStorage.decryptString;
|
jest.resetModules();
|
||||||
safeStorage.decryptString = jest.fn(() => {
|
const mockStorage = {
|
||||||
throw new Error('Decryption failed');
|
isEncryptionAvailable: jest.fn(() => true),
|
||||||
});
|
encryptString: jest.fn((data: string) => Buffer.from(`encrypted:${data}`)),
|
||||||
|
decryptString: jest.fn(() => {
|
||||||
|
throw new Error('Decryption failed');
|
||||||
|
})
|
||||||
|
};
|
||||||
|
mockWindow.require = jest.fn(() => ({ safeStorage: mockStorage }));
|
||||||
|
|
||||||
|
const { decryptApiKey: decrypt } = require('../src/utils/encryption-utils');
|
||||||
const encrypted = 'encrypted:aW52YWxpZA=='; // Invalid encrypted data
|
const encrypted = 'encrypted:aW52YWxpZA=='; // Invalid encrypted data
|
||||||
|
|
||||||
expect(() => decryptApiKey(encrypted)).toThrow('Failed to decrypt API key');
|
expect(() => decrypt(encrypted)).toThrow('Failed to decrypt API key');
|
||||||
safeStorage.decryptString = originalDecrypt; // Restore
|
|
||||||
|
// Restore original mock
|
||||||
|
mockWindow.require = mockWindowRequire;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isEncryptionAvailable', () => {
|
describe('isEncryptionAvailable', () => {
|
||||||
it('should return true when encryption is available', () => {
|
it('should return true when encryption is available', () => {
|
||||||
const { isEncryptionAvailable } = require('../src/utils/encryption-utils');
|
jest.resetModules();
|
||||||
const { safeStorage } = require('electron');
|
const mockStorage = {
|
||||||
|
isEncryptionAvailable: jest.fn(() => true),
|
||||||
|
encryptString: jest.fn(),
|
||||||
|
decryptString: jest.fn()
|
||||||
|
};
|
||||||
|
mockWindow.require = jest.fn(() => ({ safeStorage: mockStorage }));
|
||||||
|
|
||||||
safeStorage.isEncryptionAvailable.mockReturnValueOnce(true);
|
const { isEncryptionAvailable: checkAvail } = require('../src/utils/encryption-utils');
|
||||||
expect(isEncryptionAvailable()).toBe(true);
|
expect(checkAvail()).toBe(true);
|
||||||
|
|
||||||
|
// Restore
|
||||||
|
mockWindow.require = mockWindowRequire;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when encryption is not available', () => {
|
it('should return false when encryption is not available', () => {
|
||||||
const { isEncryptionAvailable } = require('../src/utils/encryption-utils');
|
jest.resetModules();
|
||||||
const { safeStorage } = require('electron');
|
const mockStorage = {
|
||||||
|
isEncryptionAvailable: jest.fn(() => false),
|
||||||
|
encryptString: jest.fn(),
|
||||||
|
decryptString: jest.fn()
|
||||||
|
};
|
||||||
|
mockWindow.require = jest.fn(() => ({ safeStorage: mockStorage }));
|
||||||
|
|
||||||
safeStorage.isEncryptionAvailable.mockReturnValueOnce(false);
|
const { isEncryptionAvailable: checkAvail } = require('../src/utils/encryption-utils');
|
||||||
expect(isEncryptionAvailable()).toBe(false);
|
expect(checkAvail()).toBe(false);
|
||||||
|
|
||||||
|
// Restore
|
||||||
|
mockWindow.require = mockWindowRequire;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when safeStorage is null', () => {
|
it('should return false when safeStorage is null', () => {
|
||||||
// This tests the case where Electron is not available
|
|
||||||
// We need to reload the module with electron unavailable
|
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
|
mockWindow.require = jest.fn(() => ({ safeStorage: null }));
|
||||||
|
|
||||||
jest.mock('electron', () => ({
|
const { isEncryptionAvailable: checkAvail } = require('../src/utils/encryption-utils');
|
||||||
safeStorage: null
|
expect(checkAvail()).toBe(false);
|
||||||
}));
|
|
||||||
|
|
||||||
const { isEncryptionAvailable } = require('../src/utils/encryption-utils');
|
|
||||||
expect(isEncryptionAvailable()).toBe(false);
|
|
||||||
|
|
||||||
// Restore original mock
|
// Restore original mock
|
||||||
jest.resetModules();
|
mockWindow.require = mockWindowRequire;
|
||||||
jest.mock('electron', () => ({
|
|
||||||
safeStorage: {
|
|
||||||
isEncryptionAvailable: jest.fn(() => true),
|
|
||||||
encryptString: jest.fn((data: string) => Buffer.from(`encrypted:${data}`)),
|
|
||||||
decryptString: jest.fn((buffer: Buffer) => {
|
|
||||||
const str = buffer.toString();
|
|
||||||
return str.replace('encrypted:', '');
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when isEncryptionAvailable method is missing', () => {
|
it('should return false when isEncryptionAvailable method is missing', () => {
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
|
const mockStorage = {
|
||||||
|
// Missing isEncryptionAvailable method
|
||||||
|
encryptString: jest.fn(),
|
||||||
|
decryptString: jest.fn()
|
||||||
|
};
|
||||||
|
mockWindow.require = jest.fn(() => ({ safeStorage: mockStorage }));
|
||||||
|
|
||||||
jest.mock('electron', () => ({
|
const { isEncryptionAvailable: checkAvail } = require('../src/utils/encryption-utils');
|
||||||
safeStorage: {
|
expect(checkAvail()).toBe(false);
|
||||||
// Missing isEncryptionAvailable method
|
|
||||||
encryptString: jest.fn(),
|
|
||||||
decryptString: jest.fn()
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { isEncryptionAvailable } = require('../src/utils/encryption-utils');
|
|
||||||
expect(isEncryptionAvailable()).toBe(false);
|
|
||||||
|
|
||||||
// Restore
|
// Restore
|
||||||
jest.resetModules();
|
mockWindow.require = mockWindowRequire;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,12 +238,13 @@ describe('Encryption Utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.resetModules();
|
// Restore mock after each test
|
||||||
|
mockWindow.require = mockWindowRequire;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle electron module not being available', () => {
|
it('should handle electron module not being available', () => {
|
||||||
// Mock require to throw when loading electron
|
// Mock require to throw when loading electron
|
||||||
jest.mock('electron', () => {
|
mockWindow.require = jest.fn(() => {
|
||||||
throw new Error('Electron not available');
|
throw new Error('Electron not available');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,12 +252,12 @@ describe('Encryption Utils', () => {
|
|||||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
|
||||||
// Load module with electron unavailable
|
// Load module with electron unavailable
|
||||||
const { encryptApiKey, isEncryptionAvailable } = require('../src/utils/encryption-utils');
|
const { encryptApiKey: encrypt, isEncryptionAvailable: checkAvail } = require('../src/utils/encryption-utils');
|
||||||
|
|
||||||
expect(isEncryptionAvailable()).toBe(false);
|
expect(checkAvail()).toBe(false);
|
||||||
|
|
||||||
const apiKey = 'test-key';
|
const apiKey = 'test-key';
|
||||||
const result = encryptApiKey(apiKey);
|
const result = encrypt(apiKey);
|
||||||
|
|
||||||
// Should return plaintext when electron is unavailable
|
// Should return plaintext when electron is unavailable
|
||||||
expect(result).toBe(apiKey);
|
expect(result).toBe(apiKey);
|
||||||
@@ -195,21 +266,19 @@ describe('Encryption Utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle decryption when safeStorage is null', () => {
|
it('should handle decryption when safeStorage is null', () => {
|
||||||
jest.mock('electron', () => ({
|
mockWindow.require = jest.fn(() => ({ safeStorage: null }));
|
||||||
safeStorage: null
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { decryptApiKey } = require('../src/utils/encryption-utils');
|
const { decryptApiKey: decrypt } = require('../src/utils/encryption-utils');
|
||||||
|
|
||||||
const encrypted = 'encrypted:aW52YWxpZA==';
|
const encrypted = 'encrypted:aW52YWxpZA==';
|
||||||
|
|
||||||
expect(() => decryptApiKey(encrypted)).toThrow('Failed to decrypt API key');
|
expect(() => decrypt(encrypted)).toThrow('Failed to decrypt API key');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should log warning when encryption not available on first load', () => {
|
it('should log warning when encryption not available on first load', () => {
|
||||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
|
||||||
jest.mock('electron', () => {
|
mockWindow.require = jest.fn(() => {
|
||||||
throw new Error('Module not found');
|
throw new Error('Module not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -225,35 +294,32 @@ describe('Encryption Utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should gracefully handle plaintext keys when encryption unavailable', () => {
|
it('should gracefully handle plaintext keys when encryption unavailable', () => {
|
||||||
jest.mock('electron', () => ({
|
mockWindow.require = jest.fn(() => ({ safeStorage: null }));
|
||||||
safeStorage: null
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { encryptApiKey, decryptApiKey } = require('../src/utils/encryption-utils');
|
const { encryptApiKey: encrypt, decryptApiKey: decrypt } = require('../src/utils/encryption-utils');
|
||||||
|
|
||||||
const apiKey = 'plain-api-key';
|
const apiKey = 'plain-api-key';
|
||||||
|
|
||||||
// Encrypt should return plaintext
|
// Encrypt should return plaintext
|
||||||
const encrypted = encryptApiKey(apiKey);
|
const encrypted = encrypt(apiKey);
|
||||||
expect(encrypted).toBe(apiKey);
|
expect(encrypted).toBe(apiKey);
|
||||||
|
|
||||||
// Decrypt plaintext should return as-is
|
// Decrypt plaintext should return as-is
|
||||||
const decrypted = decryptApiKey(apiKey);
|
const decrypted = decrypt(apiKey);
|
||||||
expect(decrypted).toBe(apiKey);
|
expect(decrypted).toBe(apiKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should warn when falling back to plaintext storage', () => {
|
it('should warn when falling back to plaintext storage', () => {
|
||||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
|
||||||
jest.mock('electron', () => ({
|
const mockStorage = {
|
||||||
safeStorage: {
|
isEncryptionAvailable: jest.fn(() => false)
|
||||||
isEncryptionAvailable: jest.fn(() => false)
|
};
|
||||||
}
|
mockWindow.require = jest.fn(() => ({ safeStorage: mockStorage }));
|
||||||
}));
|
|
||||||
|
|
||||||
const { encryptApiKey } = require('../src/utils/encryption-utils');
|
const { encryptApiKey: encrypt } = require('../src/utils/encryption-utils');
|
||||||
|
|
||||||
encryptApiKey('test-key');
|
encrypt('test-key');
|
||||||
|
|
||||||
expect(consoleSpy).toHaveBeenCalledWith(
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('Encryption not available')
|
expect.stringContaining('Encryption not available')
|
||||||
|
|||||||
@@ -1,18 +1,53 @@
|
|||||||
import { generateApiKey } from '../src/utils/auth-utils';
|
import { generateApiKey } from '../src/utils/auth-utils';
|
||||||
import { encryptApiKey, decryptApiKey } from '../src/utils/encryption-utils';
|
|
||||||
import { DEFAULT_SETTINGS } from '../src/types/settings-types';
|
import { DEFAULT_SETTINGS } from '../src/types/settings-types';
|
||||||
|
|
||||||
// Mock electron
|
// Mock safeStorage implementation
|
||||||
jest.mock('electron', () => ({
|
const mockSafeStorage = {
|
||||||
safeStorage: {
|
isEncryptionAvailable: jest.fn(() => true),
|
||||||
isEncryptionAvailable: jest.fn(() => true),
|
encryptString: jest.fn((data: string) => Buffer.from(`encrypted:${data}`)),
|
||||||
encryptString: jest.fn((data: string) => Buffer.from(`encrypted:${data}`)),
|
decryptString: jest.fn((buffer: Buffer) => buffer.toString().replace('encrypted:', ''))
|
||||||
decryptString: jest.fn((buffer: Buffer) => {
|
};
|
||||||
const str = buffer.toString();
|
|
||||||
return str.replace('encrypted:', '');
|
// Setup window.require mock
|
||||||
})
|
const mockWindowRequire = jest.fn((module: string) => {
|
||||||
|
if (module === 'electron') {
|
||||||
|
return { safeStorage: mockSafeStorage };
|
||||||
}
|
}
|
||||||
}));
|
throw new Error(`Module not found: ${module}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create mock window object for Node environment
|
||||||
|
const mockWindow: Window & { require?: unknown } = {
|
||||||
|
require: mockWindowRequire
|
||||||
|
} as unknown as Window & { require?: unknown };
|
||||||
|
|
||||||
|
// Store original global window
|
||||||
|
const originalWindow = (globalThis as unknown as { window?: unknown }).window;
|
||||||
|
|
||||||
|
// Set up window.require before tests run
|
||||||
|
beforeAll(() => {
|
||||||
|
(globalThis as unknown as { window: typeof mockWindow }).window = mockWindow;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up after all tests
|
||||||
|
afterAll(() => {
|
||||||
|
if (originalWindow === undefined) {
|
||||||
|
delete (globalThis as unknown as { window?: unknown }).window;
|
||||||
|
} else {
|
||||||
|
(globalThis as unknown as { window: typeof originalWindow }).window = originalWindow;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Import after mock is set up
|
||||||
|
let encryptApiKey: typeof import('../src/utils/encryption-utils').encryptApiKey;
|
||||||
|
let decryptApiKey: typeof import('../src/utils/encryption-utils').decryptApiKey;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
const encryptionUtils = require('../src/utils/encryption-utils');
|
||||||
|
encryptApiKey = encryptionUtils.encryptApiKey;
|
||||||
|
decryptApiKey = encryptionUtils.decryptApiKey;
|
||||||
|
});
|
||||||
|
|
||||||
describe('Settings Migration', () => {
|
describe('Settings Migration', () => {
|
||||||
describe('API key initialization', () => {
|
describe('API key initialization', () => {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jest.mock('../src/utils/path-utils', () => ({
|
|||||||
// Mock LinkUtils for link validation tests
|
// Mock LinkUtils for link validation tests
|
||||||
jest.mock('../src/utils/link-utils', () => ({
|
jest.mock('../src/utils/link-utils', () => ({
|
||||||
LinkUtils: {
|
LinkUtils: {
|
||||||
validateLinks: jest.fn().mockResolvedValue({
|
validateLinks: jest.fn().mockReturnValue({
|
||||||
valid: [],
|
valid: [],
|
||||||
brokenNotes: [],
|
brokenNotes: [],
|
||||||
brokenHeadings: [],
|
brokenHeadings: [],
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"1.0.0": "0.15.0",
|
"1.0.0": "0.15.0",
|
||||||
"1.0.1": "0.15.0",
|
"1.0.1": "0.15.0",
|
||||||
"1.1.0": "0.15.0",
|
"1.1.0": "0.15.0",
|
||||||
"1.1.1": "0.15.0"
|
"1.1.1": "0.15.0",
|
||||||
|
"1.1.2": "0.15.0",
|
||||||
|
"1.1.3": "0.15.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user