🎨 Plugin lint

This commit is contained in:
Erin Schnabel 2025-11-17 22:09:55 -05:00
parent d46be3cce5
commit cedca2173d
No known key found for this signature in database
10 changed files with 4697 additions and 64 deletions

35
SECURITY.md Normal file
View file

@ -0,0 +1,35 @@
# Security Policy
I only support the latest version of this plugin. Given the rapid
evolution of the MCP protocol and dependencies, running/maintaining older
versions isn't feasible.
## Security Considerations
This plugin runs a local HTTP server on your machine to expose vault
access via the MCP protocol:
- **Network exposure**: The server listens on localhost by default but
supports CORS for remote access (e.g., via Tailscale)
- **Authentication**: Currently there is no authentication mechanism - any
client that can reach the configured port can access your vault
- **Data privacy**: All data stays on your machine; the plugin does not
connect to external services or collect telemetry
- **Bridge script**: The `mcp-bridge.js` script runs as a Node.js process
and bridges stdio to HTTP
## Reporting a Vulnerability
Please report suspected security issues privately using GitHub's "Report a
vulnerability" link in the repository sidebar.
Do **not** open a public issue for suspected vulnerabilities.
When reporting, please include:
- A description of the issue and affected versions
- Steps to reproduce (ideally a minimal proof-of-concept)
- Assessment of potential impact (network exposure, data access, etc.)
- Your contact information
- Any specific requests, such as anonymity for you and/or the
organization you represent

35
eslint.config.mjs Normal file
View file

@ -0,0 +1,35 @@
// eslint.config.mjs
import globals from "globals";
import tsparser from "@typescript-eslint/parser";
import { defineConfig, globalIgnores } from "eslint/config";
import obsidianmd from "eslint-plugin-obsidianmd";
export default defineConfig([
...obsidianmd.configs.recommended,
globalIgnores([
"tests/",
"*.mjs",
"package.json"
]),
{
files: ["src/**/*.ts"],
languageOptions: {
parser: tsparser,
parserOptions: {
project: "./tsconfig.json"
},
globals: { ...globals.node, ...globals.browser },
},
// Optional project overrides
rules: {
"obsidianmd/ui/sentence-case": [
"warn",
{
brands: ["Deck Notes", "journal/coping\nactivities/morning", "example"],
acronyms: ["DN", "H2"],
enforceCamelCaseLower: true,
},
],
},
},
]);

4565
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,12 +8,14 @@
"dev": "node esbuild.config.mjs",
"fix": "npx @biomejs/biome check --write ./src",
"format": "npx @biomejs/biome format ./src",
"eslint": "npx eslint",
"lint": "npx @biomejs/biome lint ./src",
"prebuild": "npx @biomejs/biome check ./src",
"build": "node esbuild.config.mjs production",
"postbuild": "cp -v manifest.json README.md build",
"preversion": "npm run build",
"version": "auto-changelog -p"
"version": "auto-changelog -p",
"brat-notes": "run() { auto-changelog --stdout --hide-credit --hide-empty-releases --template .github/changelog.hbs -v $1 --starting-version $1 > release-notes.md; }; run"
},
"keywords": [
"obsidian",
@ -28,10 +30,14 @@
"devDependencies": {
"@biomejs/biome": "2.2.4",
"@types/node": "^24.3.1",
"@typescript-eslint/parser": "^8.46.4",
"auto-changelog": "^2.5.0",
"builtin-modules": "^5.0.0",
"esbuild": "^0.25.9",
"obsidian": "^1.8.7",
"eslint": "^9.39.1",
"eslint-plugin-obsidianmd": "^0.1.9",
"moment": "^2.30.1",
"obsidian": "^1.10.2",
"tslib": "^2.8.1",
"typescript": "5.9.2"
},

View file

@ -13,7 +13,7 @@ export class DeckNotesApi {
* @param deck Tag for deck selection (e.g., "activities")
* @returns The card embed text, or null if no cards available
*/
embedCard(deck: string | undefined = undefined): string | null {
embedCard(deck?: string): string | null {
return this.plugin.createEmbedText(deck);
}

View file

@ -80,12 +80,26 @@ export class CardParser {
*/
extractFileLevelTags(fileCache: CachedMetadata | null): string[] {
const tags: string[] = [];
const frontmatterTags = fileCache?.frontmatter?.tags || [];
const frontmatterTags = fileCache?.frontmatter?.tags as
| string[]
| Record<string, unknown>
| undefined;
for (const tag of frontmatterTags) {
const normalized = this.normalizeTag(tag);
if (normalized) {
tags.push(normalized);
if (!frontmatterTags) {
return tags;
}
// Handle both array and object formats
const tagList = Array.isArray(frontmatterTags)
? frontmatterTags
: Object.keys(frontmatterTags);
for (const tag of tagList) {
if (typeof tag === "string") {
const normalized = this.normalizeTag(tag);
if (normalized) {
tags.push(normalized);
}
}
}

View file

@ -2,7 +2,7 @@ import { type App, ButtonComponent, MarkdownRenderer, Modal } from "obsidian";
import type { Card } from "./@types/settings";
import type DeckNotesPlugin from "./dn-Plugin";
export class FlashcardModal extends Modal {
export class CardModal extends Modal {
plugin: DeckNotesPlugin;
card: Card | null;
deckTag: string | undefined;
@ -40,14 +40,16 @@ export class FlashcardModal extends Modal {
// Render card content as markdown
const contentDiv = contentEl.createEl("div", {
cls: "flashcard-content",
cls: "card-content",
});
MarkdownRenderer.render(
// Modal lifecycle is tied to plugin, safe to use as component
void MarkdownRenderer.render(
this.app,
this.card.content,
contentDiv,
this.card.filePath,
// eslint-disable-next-line obsidianmd/no-plugin-as-component
this.plugin,
);
@ -65,7 +67,7 @@ export class FlashcardModal extends Modal {
const availableTags = this.plugin.api.getTags();
if (availableTags.length > 1) {
new ButtonComponent(buttonContainer)
.setButtonText("Switch Deck")
.setButtonText("Switch deck")
.onClick(() => {
this.showDeckSwitcher();
});
@ -73,7 +75,7 @@ export class FlashcardModal extends Modal {
// Next Card button
new ButtonComponent(buttonContainer)
.setButtonText("Next Card")
.setButtonText("Next card")
.setCta()
.onClick(() => {
this.showNextCard();

View file

@ -3,7 +3,7 @@ import type { Card, DeckNotesData, DeckNotesSettings } from "./@types/settings";
import { DeckNotesApi } from "./dn-Api";
import { CardParser } from "./dn-CardParser";
import { DEFAULT_SETTINGS } from "./dn-Constants";
import { FlashcardModal } from "./dn-Modal";
import { CardModal } from "./dn-Modal";
import { DeckNotesSettingsTab } from "./dn-SettingsTab";
export default class DeckNotesPlugin extends Plugin {
@ -14,7 +14,7 @@ export default class DeckNotesPlugin extends Plugin {
api!: DeckNotesApi;
async onload() {
console.log("Loading Deck Notes plugin");
console.debug("Loading Deck Notes plugin", `v${this.manifest.version}`);
await this.loadSettings();
this.cardParser = new CardParser(this.app);
@ -32,38 +32,38 @@ export default class DeckNotesPlugin extends Plugin {
}
window.deckNotes.api = this.api;
// Command: Show Random Card
// Command: Show card
this.addCommand({
id: "show-card",
name: "Show Card",
name: "Show card",
callback: () => {
this.showFlashCard();
this.showCard();
},
});
// Command: Embed Card
this.addCommand({
id: "embed-card",
name: "Embed Card",
name: "Embed card",
editorCallback: (editor) => {
this.embedCard(editor);
},
});
// Command: Refresh Cards
// Command: Refresh cards
this.addCommand({
id: "refresh-cards",
name: "Refresh Cards",
name: "Refresh cards",
callback: async () => {
await this.scanCards();
new Notice("Flash cards refreshed");
new Notice("Cards refreshed");
},
});
});
}
onunload() {
console.log("Unloading Deck Notes plugin");
console.debug("Unloading Deck Notes plugin");
// Clear API reference
if (window.deckNotes) {
@ -72,7 +72,7 @@ export default class DeckNotesPlugin extends Plugin {
}
async loadSettings() {
const data = await this.loadData();
const data = (await this.loadData()) as DeckNotesData;
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
this.data = data || { cardViews: {} };
}
@ -101,7 +101,7 @@ export default class DeckNotesPlugin extends Plugin {
}
}
console.log(`Scanned ${this.cachedCards.length} cards`);
console.debug(`Scanned ${this.cachedCards.length} cards`);
}
private async scanFolder(folder: TFolder) {
@ -131,31 +131,33 @@ export default class DeckNotesPlugin extends Plugin {
return null;
}
// least-recent: sort by lastSeen timestamp
if (this.settings.selectionMode === "random") {
return pool[Math.floor(Math.random() * pool.length)];
}
// Least-recent: sort by lastSeen timestamp
const sorted = pool.slice().sort((a, b) => {
const aTime = this.data.cardViews[a.key] || 0;
const bTime = this.data.cardViews[b.key] || 0;
if (aTime === bTime) {
// consistent, pseudo-randomizing sort
// Consistent, pseudo-randomizing sort
return a.hash.localeCompare(b.hash);
}
return aTime - bTime;
});
return this.settings.selectionMode === "random"
? pool[Math.floor(Math.random() * pool.length)]
: sorted[0];
return sorted[0];
}
recordView(cardKey: string) {
this.data.cardViews[cardKey] = Date.now();
this.saveData({
void this.saveData({
...this.settings,
cardViews: this.data.cardViews,
});
}
showFlashCard() {
showCard() {
const deckTag = this.settings.defaultDeckTag || undefined;
const card = this.selectCard(deckTag);
@ -164,10 +166,10 @@ export default class DeckNotesPlugin extends Plugin {
return;
}
new FlashcardModal(this.app, this, card, deckTag).open();
new CardModal(this.app, this, card, deckTag).open();
}
createEmbedText(deck: string | undefined): string | null {
createEmbedText(deck?: string): string | null {
const deckTag = deck || this.settings.defaultDeckTag;
const card = this.selectCard(deckTag);
@ -193,7 +195,7 @@ export default class DeckNotesPlugin extends Plugin {
return embedText;
}
embedCard(editor: Editor, deck: string | undefined = undefined) {
embedCard(editor: Editor, deck?: string) {
const embedText = this.createEmbedText(deck);
if (!embedText) {

View file

@ -17,7 +17,9 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
}
private cloneSettings(): DeckNotesSettings {
return JSON.parse(JSON.stringify(this.plugin.settings));
return JSON.parse(
JSON.stringify(this.plugin.settings),
) as DeckNotesSettings;
}
async reset() {
@ -36,14 +38,14 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
new Setting(containerEl).setHeading().setName("Deck Notes");
new Setting(containerEl)
.setName("Save Settings")
.setName("Save settings")
.setClass("decknotes-save-reset")
.addButton((button) =>
button
.setButtonText("Reset")
.setTooltip("Reset to current saved settings")
.onClick(() => {
this.reset();
.onClick(async () => {
await this.reset();
}),
)
.addButton((button) =>
@ -61,13 +63,13 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
});
new Setting(containerEl)
.setName("Card Paths")
.setName("Card paths")
.setDesc(
"Paths to folders containing card files (one per line, relative to vault root)",
)
.addTextArea((text) =>
text
.setPlaceholder("Journal/Coping\nActivities/Morning")
.setPlaceholder("journal/coping\nactivities/morning")
.setValue(this.newSettings.cardPaths.join("\n"))
.onChange((value) => {
this.newSettings.cardPaths = value
@ -83,13 +85,13 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
});
new Setting(containerEl)
.setName("Default Deck Tag")
.setName("Default deck tag")
.setDesc(
"Default tag for 'Show Random Activity Card' command (e.g., 'activities' or 'activities/morning'). Leave empty for all cards.",
)
.addText((text) =>
text
.setPlaceholder("activities")
.setPlaceholder("Activities")
.setValue(this.newSettings.defaultDeckTag)
.onChange((value) => {
this.newSettings.defaultDeckTag = value.trim();
@ -97,7 +99,7 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName("Track Views")
.setName("Track views")
.setDesc(
"Track when cards were last viewed (enables least-recent selection)",
)
@ -110,12 +112,12 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName("Selection Mode")
.setName("Selection mode")
.setDesc("How to select cards to display")
.addDropdown((dropdown) =>
dropdown
.addOption("random", "Random")
.addOption("least-recent", "Least Recently Viewed")
.addOption("least-recent", "Least recently viewed")
.setValue(this.newSettings.selectionMode)
.onChange((value) => {
this.newSettings.selectionMode = value as
@ -125,13 +127,13 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName("Callout Type")
.setName("Callout type")
.setDesc(
"Callout type for embedded cards (e.g., note, tip, warning)",
)
.addText((text) =>
text
.setPlaceholder("readaloud")
.setPlaceholder("example")
.setValue(this.newSettings.calloutType)
.onChange((value) => {
this.newSettings.calloutType = value.trim();
@ -153,6 +155,6 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
/** Save on exit */
hide(): void {
this.save();
void this.save();
}
}

View file

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ES2018",
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2018", "DOM"],
"moduleResolution": "bundler",