refactor: rename plugin and update description; remove unused CSS file

This commit is contained in:
callmeaderp 2025-06-08 13:58:50 -04:00
parent 6d09ce3e39
commit cbc9d095d6
7 changed files with 3072 additions and 109 deletions

162
CLAUDE.md Normal file
View File

@ -0,0 +1,162 @@
# MOC System Plugin
## Overview
This is a custom Obsidian plugin designed to automate and streamline a MOC (Map of Content) based note-taking system. The plugin focuses on efficiency by providing context-aware commands and automatic organization of notes into a hierarchical structure.
## Goals
The primary goal of this plugin is to automate the user's MOC-based system for organizing notes in Obsidian, with these specific objectives:
1. **Single-command note creation** - One keyboard shortcut handles all note creation needs based on context
2. **Dynamic content organization** - MOCs only show sections that contain content, maintaining clean and minimal structure
3. **Efficient prompt management** - Specialized system for managing LLM prompts with versioning and multi-chat link support
4. **Automated maintenance** - Auto-cleanup of broken links and automatic folder structure creation
## System Design
### File Organization Structure
- **Top-level MOCs**: Created in vault root directory
- **Sub-MOCs**: Stored in `MOCs/` folder
- **Notes**: Stored in `Notes/` folder
- **Resources**: Stored in `Resources/` folder
- **Prompts**: Stored in `Prompts/` folder (includes both hubs and iterations)
### MOC Structure
MOCs are identified by the `#moc` tag in their frontmatter. They start empty and dynamically display only the sections that contain content, in this fixed order:
1. MOCs (sub-MOCs)
2. Notes
3. Resources
4. Prompts
### Prompt System
The prompt system is designed for iterative LLM conversations:
- **Prompt Hub**: Main note for a prompt topic (e.g., `AI Assistant.md`)
- Contains links to all iterations
- Includes `llm-links` code block for storing chat URLs
- **Iterations**: Individual versions (e.g., `AI Assistant v1.md`, `AI Assistant v2 - Added error handling.md`)
- Can be duplicated from any version
- Automatically increments to next available version number
- Optional description can be added to title
## Features
### 1. Context-Aware Creation Command
**Command**: "Create MOC or add content"
- When not in a MOC: Creates a new top-level MOC
- When in a MOC: Shows modal with options to create:
- Sub-MOC
- Note
- Resource
- Prompt
### 2. Prompt Iteration Duplication
**Command**: "Duplicate prompt iteration"
- Works when viewing any prompt iteration file
- Creates copy with next version number
- Shows modal for optional description
- Updates the prompt hub automatically
### 3. Multi-Link Opening
**Command**: "Open all LLM links"
- Works when viewing a prompt hub
- Parses `llm-links` code block
- Opens all URLs in new browser tabs
### 4. Automatic Features
- **Folder Structure**: Creates required folders on plugin load
- **Section Management**: Adds sections to MOCs only when first item is created
- **Link Cleanup**: Removes broken links when files are deleted
## Implementation Details
### Core Architecture
The plugin extends Obsidian's Plugin class with these key components:
```typescript
export default class MOCSystemPlugin extends Plugin {
// Main plugin class
}
```
### Key Methods
#### Content Creation Methods
- `createMOC()`: Creates top-level MOC with frontmatter tags
- `createSubMOC()`: Creates MOC in MOCs/ folder and links from parent
- `createNote()`: Creates note in Notes/ folder and links from parent MOC
- `createResource()`: Creates resource in Resources/ folder and links from parent
- `createPrompt()`: Creates prompt hub with first iteration and LLM links block
#### Section Management
- `addToMOCSection()`: Intelligently adds links to MOC sections
- Creates section if it doesn't exist
- Maintains proper section ordering
- Inserts links at appropriate position
#### Prompt System
- `duplicatePromptIteration()`:
- Parses filename to extract base name and version
- Finds highest existing version number
- Creates new file with incremented version
- Updates prompt hub with new iteration link
- `updatePromptHub()`: Adds new iteration links to hub file
- `openLLMLinks()`: Extracts URLs from code block and opens in browser
#### Maintenance
- `cleanupBrokenLinks()`: Removes references to deleted files
- `ensureFolderStructure()`: Creates required folders if missing
### File Detection Methods
- `isMOC()`: Checks for `#moc` tag in frontmatter
- `isPromptIteration()`: Detects files with version pattern (v1, v2, etc.)
- `isPromptHub()`: Identifies prompt files that aren't iterations
### Modal Dialogs
The plugin includes several custom modals for user input:
1. **CreateMOCModal**: For creating new top-level MOCs
2. **AddToMOCModal**: Shows options when adding content to existing MOC
3. **CreateItemModal**: Generic input for creating notes/resources/etc.
4. **PromptDescriptionModal**: Optional description when duplicating prompts
### Event Handling
- Registers file deletion event to trigger automatic link cleanup
- Uses command callbacks to check active file context
- Implements keyboard shortcuts (Enter key) in all modals
## Technical Decisions
1. **Frontend-only approach**: All logic in main.ts, no settings or complex state management
2. **Tag-based MOC identification**: Uses frontmatter tags instead of naming conventions for flexibility
3. **Dynamic sections**: Sections only appear when needed, keeping MOCs clean
4. **Regex-based parsing**: For version detection and link patterns
5. **Batch link opening**: Uses window.open() in a loop for multi-link functionality
## Current Status
The plugin has been fully implemented with all requested features:
- ✅ Context-aware creation command
- ✅ Prompt iteration system with versioning
- ✅ Multi-link opening for LLM chats
- ✅ Dynamic section management
- ✅ Automatic link cleanup
- ✅ Folder structure creation
The plugin has been built and is ready for testing in Obsidian.
## History
*Initial implementation completed in first session - no previous history*

View File

@ -1,16 +1,6 @@
# Obsidian Sample Plugin
# My Obsidian System Plugin
This is a sample plugin for Obsidian (https://obsidian.md).
This project uses TypeScript to provide type checking and documentation.
The repo depends on the latest plugin API (obsidian.d.ts) in TypeScript Definition format, which contains TSDoc comments describing what it does.
This sample plugin demonstrates some of the basic functionality the plugin API can do.
- Adds a ribbon icon, which shows a Notice when clicked.
- Adds a command "Open Sample Modal" which opens a Modal.
- Adds a plugin setting tab to the settings page.
- Registers a global click event and output 'click' to the console.
- Registers a global interval which logs 'setInterval' to the console.
An Obsidian plugin for system integration and automation.
## First time developing plugins?

576
main.ts
View File

@ -1,85 +1,81 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
import { App, Modal, Notice, Plugin, TFile, TFolder, normalizePath, MarkdownView } from 'obsidian';
// Remember to rename these classes and interfaces!
interface MyPluginSettings {
mySetting: string;
interface PluginSettings {
}
const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: 'default'
const DEFAULT_SETTINGS: PluginSettings = {
}
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
const FOLDERS = {
MOCs: 'MOCs',
Notes: 'Notes',
Resources: 'Resources',
Prompts: 'Prompts'
} as const;
const SECTION_ORDER = ['MOCs', 'Notes', 'Resources', 'Prompts'] as const;
type SectionType = typeof SECTION_ORDER[number];
export default class MOCSystemPlugin extends Plugin {
settings: PluginSettings;
async onload() {
await this.loadSettings();
await this.ensureFolderStructure();
// This creates an icon in the left ribbon.
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
// Called when the user clicks the icon.
new Notice('This is a notice!');
// Main command for context-aware creation
this.addCommand({
id: 'moc-context-create',
name: 'Create MOC or add content',
callback: () => this.handleContextCreate()
});
// Perform additional things with the ribbon
ribbonIconEl.addClass('my-plugin-ribbon-class');
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text');
// This adds a simple command that can be triggered anywhere
// Command to duplicate prompt iteration
this.addCommand({
id: 'open-sample-modal-simple',
name: 'Open sample modal (simple)',
callback: () => {
new SampleModal(this.app).open();
}
});
// This adds an editor command that can perform some operation on the current editor instance
this.addCommand({
id: 'sample-editor-command',
name: 'Sample editor command',
editorCallback: (editor: Editor, view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('Sample Editor Command');
}
});
// This adds a complex command that can check whether the current state of the app allows execution of the command
this.addCommand({
id: 'open-sample-modal-complex',
name: 'Open sample modal (complex)',
id: 'duplicate-prompt-iteration',
name: 'Duplicate prompt iteration',
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (markdownView) {
// If checking is true, we're simply "checking" if the command can be run.
// If checking is false, then we want to actually perform the operation.
const activeFile = this.app.workspace.getActiveFile();
if (activeFile && this.isPromptIteration(activeFile)) {
if (!checking) {
new SampleModal(this.app).open();
this.duplicatePromptIteration(activeFile);
}
// This command will only show up in Command Palette when the check function returns true
return true;
}
return false;
}
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new SampleSettingTab(this.app, this));
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
// Using this function will automatically remove the event listener when this plugin is disabled.
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
console.log('click', evt);
// Command to open all LLM links
this.addCommand({
id: 'open-llm-links',
name: 'Open all LLM links',
checkCallback: (checking: boolean) => {
const activeFile = this.app.workspace.getActiveFile();
if (activeFile && this.isPromptHub(activeFile)) {
if (!checking) {
this.openLLMLinks(activeFile);
}
return true;
}
return false;
}
});
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
// Auto-cleanup on file deletion
this.registerEvent(
this.app.vault.on('delete', (file) => {
if (file instanceof TFile) {
this.cleanupBrokenLinks(file);
}
})
);
}
onunload() {
}
async loadSettings() {
@ -89,46 +85,464 @@ export default class MyPlugin extends Plugin {
async saveSettings() {
await this.saveData(this.settings);
}
async ensureFolderStructure() {
for (const folder of Object.values(FOLDERS)) {
const folderPath = normalizePath(folder);
if (!this.app.vault.getAbstractFileByPath(folderPath)) {
await this.app.vault.createFolder(folderPath);
}
}
}
async handleContextCreate() {
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile || !this.isMOC(activeFile)) {
// Not in a MOC, create new MOC
new CreateMOCModal(this.app, async (name: string) => {
await this.createMOC(name);
}).open();
} else {
// In a MOC, show options to add content
new AddToMOCModal(this.app, activeFile, this).open();
}
}
async createMOC(name: string): Promise<TFile> {
const fileName = `${name}.md`;
const content = `---\ntags:\n - moc\n---\n`;
const file = await this.app.vault.create(fileName, content);
await this.app.workspace.getLeaf().openFile(file);
new Notice(`Created MOC: ${name}`);
return file;
}
async createSubMOC(parentMOC: TFile, name: string): Promise<TFile> {
const fileName = `${FOLDERS.MOCs}/${name}.md`;
const content = `---\ntags:\n - moc\n---\n`;
const file = await this.app.vault.create(normalizePath(fileName), content);
await this.addToMOCSection(parentMOC, 'MOCs', file);
new Notice(`Created sub-MOC: ${name}`);
return file;
}
async createNote(parentMOC: TFile, name: string): Promise<TFile> {
const fileName = `${FOLDERS.Notes}/${name}.md`;
const content = '';
const file = await this.app.vault.create(normalizePath(fileName), content);
await this.addToMOCSection(parentMOC, 'Notes', file);
new Notice(`Created note: ${name}`);
return file;
}
async createResource(parentMOC: TFile, name: string): Promise<TFile> {
const fileName = `${FOLDERS.Resources}/${name}.md`;
const content = '';
const file = await this.app.vault.create(normalizePath(fileName), content);
await this.addToMOCSection(parentMOC, 'Resources', file);
new Notice(`Created resource: ${name}`);
return file;
}
async createPrompt(parentMOC: TFile, name: string): Promise<TFile> {
// Create prompt hub
const hubFileName = `${FOLDERS.Prompts}/${name}.md`;
const hubContent = `# ${name}\n\n## Iterations\n\n- [[${name} v1]]\n\n## LLM Links\n\n\`\`\`llm-links\n\n\`\`\`\n`;
const hubFile = await this.app.vault.create(normalizePath(hubFileName), hubContent);
// Create first iteration
const iterationFileName = `${FOLDERS.Prompts}/${name} v1.md`;
const iterationContent = '';
await this.app.vault.create(normalizePath(iterationFileName), iterationContent);
await this.addToMOCSection(parentMOC, 'Prompts', hubFile);
new Notice(`Created prompt: ${name}`);
return hubFile;
}
async addToMOCSection(moc: TFile, section: SectionType, newFile: TFile) {
const content = await this.app.vault.read(moc);
const lines = content.split('\n');
// Find or create section
let sectionIndex = -1;
let insertIndex = lines.length;
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim() === `## ${section}`) {
sectionIndex = i;
break;
}
}
if (sectionIndex === -1) {
// Section doesn't exist, find where to insert it
const currentSectionIndices: Map<SectionType, number> = new Map();
// Find existing sections
for (let i = 0; i < lines.length; i++) {
for (const sectionName of SECTION_ORDER) {
if (lines[i].trim() === `## ${sectionName}`) {
currentSectionIndices.set(sectionName, i);
}
}
}
// Find where to insert new section
insertIndex = lines.length;
for (let i = SECTION_ORDER.indexOf(section) + 1; i < SECTION_ORDER.length; i++) {
if (currentSectionIndices.has(SECTION_ORDER[i])) {
insertIndex = currentSectionIndices.get(SECTION_ORDER[i])!;
break;
}
}
// Insert section header
const newSection = [`## ${section}`, '', `- [[${newFile.basename}]]`, ''];
lines.splice(insertIndex, 0, ...newSection);
} else {
// Section exists, add link to it
let linkInsertIndex = sectionIndex + 1;
// Skip empty lines after header
while (linkInsertIndex < lines.length && lines[linkInsertIndex].trim() === '') {
linkInsertIndex++;
}
// Find end of section
while (linkInsertIndex < lines.length &&
!lines[linkInsertIndex].startsWith('## ') &&
lines[linkInsertIndex].trim() !== '') {
linkInsertIndex++;
}
// Insert before empty line or next section
lines.splice(linkInsertIndex, 0, `- [[${newFile.basename}]]`);
}
await this.app.vault.modify(moc, lines.join('\n'));
}
async duplicatePromptIteration(file: TFile) {
const match = file.basename.match(/^(.+?)\s*v(\d+)(?:\s*-\s*(.+))?$/);
if (!match) return;
const [, baseName, currentVersion] = match;
// Find all iterations to get next available version
const promptFiles = this.app.vault.getMarkdownFiles()
.filter(f => f.path.startsWith(FOLDERS.Prompts) && f.basename.startsWith(baseName));
let maxVersion = 0;
for (const pFile of promptFiles) {
const vMatch = pFile.basename.match(/v(\d+)/);
if (vMatch) {
maxVersion = Math.max(maxVersion, parseInt(vMatch[1]));
}
}
const nextVersion = maxVersion + 1;
// Ask for description
new PromptDescriptionModal(this.app, async (description: string) => {
const newName = description
? `${baseName} v${nextVersion} - ${description}`
: `${baseName} v${nextVersion}`;
const newPath = `${FOLDERS.Prompts}/${newName}.md`;
const content = await this.app.vault.read(file);
const newFile = await this.app.vault.create(normalizePath(newPath), content);
// Update hub file
await this.updatePromptHub(baseName, newFile);
await this.app.workspace.getLeaf().openFile(newFile);
new Notice(`Created iteration: ${newName}`);
}).open();
}
async updatePromptHub(baseName: string, newIteration: TFile) {
const hubPath = `${FOLDERS.Prompts}/${baseName}.md`;
const hubFile = this.app.vault.getAbstractFileByPath(normalizePath(hubPath));
if (hubFile instanceof TFile) {
const content = await this.app.vault.read(hubFile);
const lines = content.split('\n');
// Find iterations section
let iterIndex = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim() === '## Iterations') {
iterIndex = i;
break;
}
}
if (iterIndex !== -1) {
// Find where to insert
let insertIndex = iterIndex + 1;
while (insertIndex < lines.length &&
!lines[insertIndex].startsWith('## ') &&
lines[insertIndex].trim() !== '') {
insertIndex++;
}
// Insert before empty line or next section
lines.splice(insertIndex, 0, `- [[${newIteration.basename}]]`);
await this.app.vault.modify(hubFile, lines.join('\n'));
}
}
}
async openLLMLinks(file: TFile) {
const content = await this.app.vault.read(file);
const linkBlockMatch = content.match(/```llm-links\n([\s\S]*?)\n```/);
if (linkBlockMatch) {
const links = linkBlockMatch[1]
.split('\n')
.map(line => line.trim())
.filter(line => line.startsWith('http'));
if (links.length === 0) {
new Notice('No links found in llm-links block');
return;
}
// Open all links
for (const link of links) {
window.open(link, '_blank');
}
new Notice(`Opened ${links.length} links`);
} else {
new Notice('No llm-links block found');
}
}
async cleanupBrokenLinks(deletedFile: TFile) {
const allFiles = this.app.vault.getMarkdownFiles();
for (const file of allFiles) {
const content = await this.app.vault.read(file);
const linkPattern = new RegExp(`\\[\\[${deletedFile.basename}\\]\\]`, 'g');
if (linkPattern.test(content)) {
const lines = content.split('\n');
const newLines = lines.filter(line => !line.includes(`[[${deletedFile.basename}]]`));
if (lines.length !== newLines.length) {
await this.app.vault.modify(file, newLines.join('\n'));
}
}
}
}
isMOC(file: TFile): boolean {
const cache = this.app.metadataCache.getFileCache(file);
return cache?.frontmatter?.tags?.includes('moc') ?? false;
}
isPromptIteration(file: TFile): boolean {
return file.path.startsWith(FOLDERS.Prompts) && /v\d+/.test(file.basename);
}
isPromptHub(file: TFile): boolean {
return file.path.startsWith(FOLDERS.Prompts) && !this.isPromptIteration(file);
}
}
class SampleModal extends Modal {
constructor(app: App) {
class CreateMOCModal extends Modal {
constructor(app: App, private onSubmit: (name: string) => void) {
super(app);
}
onOpen() {
const {contentEl} = this;
contentEl.setText('Woah!');
const { contentEl } = this;
contentEl.createEl('h2', { text: 'Create new MOC' });
const inputEl = contentEl.createEl('input', {
type: 'text',
placeholder: 'MOC name...'
});
inputEl.style.width = '100%';
inputEl.focus();
inputEl.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && inputEl.value) {
this.onSubmit(inputEl.value);
this.close();
}
});
const buttonEl = contentEl.createEl('button', { text: 'Create' });
buttonEl.addEventListener('click', () => {
if (inputEl.value) {
this.onSubmit(inputEl.value);
this.close();
}
});
}
onClose() {
const {contentEl} = this;
const { contentEl } = this;
contentEl.empty();
}
}
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
class AddToMOCModal extends Modal {
constructor(
app: App,
private moc: TFile,
private plugin: MOCSystemPlugin
) {
super(app);
}
display(): void {
const {containerEl} = this;
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: 'Add to MOC' });
containerEl.empty();
const options: Array<{ type: SectionType, label: string }> = [
{ type: 'MOCs', label: 'Sub-MOC' },
{ type: 'Notes', label: 'Note' },
{ type: 'Resources', label: 'Resource' },
{ type: 'Prompts', label: 'Prompt' }
];
new Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(text => text
.setPlaceholder('Enter your secret')
.setValue(this.plugin.settings.mySetting)
.onChange(async (value) => {
this.plugin.settings.mySetting = value;
await this.plugin.saveSettings();
}));
options.forEach(option => {
const button = contentEl.createEl('button', {
text: `Create ${option.label}`,
cls: 'mod-cta'
});
button.style.display = 'block';
button.style.width = '100%';
button.style.marginBottom = '10px';
button.addEventListener('click', () => {
this.close();
new CreateItemModal(this.app, option.label, async (name: string) => {
switch (option.type) {
case 'MOCs':
await this.plugin.createSubMOC(this.moc, name);
break;
case 'Notes':
await this.plugin.createNote(this.moc, name);
break;
case 'Resources':
await this.plugin.createResource(this.moc, name);
break;
case 'Prompts':
await this.plugin.createPrompt(this.moc, name);
break;
}
}).open();
});
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
class CreateItemModal extends Modal {
constructor(
app: App,
private itemType: string,
private onSubmit: (name: string) => void
) {
super(app);
}
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: `Create ${this.itemType}` });
const inputEl = contentEl.createEl('input', {
type: 'text',
placeholder: `${this.itemType} name...`
});
inputEl.style.width = '100%';
inputEl.focus();
inputEl.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && inputEl.value) {
this.onSubmit(inputEl.value);
this.close();
}
});
const buttonEl = contentEl.createEl('button', { text: 'Create' });
buttonEl.addEventListener('click', () => {
if (inputEl.value) {
this.onSubmit(inputEl.value);
this.close();
}
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
class PromptDescriptionModal extends Modal {
constructor(app: App, private onSubmit: (description: string) => void) {
super(app);
}
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: 'Add iteration description (optional)' });
const inputEl = contentEl.createEl('input', {
type: 'text',
placeholder: 'Description (optional)...'
});
inputEl.style.width = '100%';
inputEl.focus();
const submitFn = () => {
this.onSubmit(inputEl.value);
this.close();
};
inputEl.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
submitFn();
}
});
const buttonContainer = contentEl.createDiv();
buttonContainer.style.display = 'flex';
buttonContainer.style.gap = '10px';
buttonContainer.style.marginTop = '10px';
const skipButton = buttonContainer.createEl('button', { text: 'Skip' });
skipButton.addEventListener('click', () => {
this.onSubmit('');
this.close();
});
const addButton = buttonContainer.createEl('button', {
text: 'Add Description',
cls: 'mod-cta'
});
addButton.addEventListener('click', submitFn);
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

View File

@ -1,11 +1,10 @@
{
"id": "sample-plugin",
"name": "Sample Plugin",
"id": "my-obsidian-system-plugin",
"name": "MOC System Plugin",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Demonstrates some of the capabilities of the Obsidian API.",
"author": "Obsidian",
"authorUrl": "https://obsidian.md",
"fundingUrl": "https://obsidian.md/pricing",
"description": "Automated MOC-based note management system",
"author": "Your Name",
"authorUrl": "",
"isDesktopOnly": false
}

2406
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "obsidian-sample-plugin",
"name": "my-obsidian-system-plugin",
"version": "1.0.0",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"description": "An Obsidian plugin for system integration and automation",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",

View File

@ -1,8 +0,0 @@
/*
This CSS file will be included with your plugin, and
available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
*/