refactor: rename plugin and update description; remove unused CSS file
This commit is contained in:
		
							parent
							
								
									6d09ce3e39
								
							
						
					
					
						commit
						cbc9d095d6
					
				| 
						 | 
				
			
			@ -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*
 | 
			
		||||
							
								
								
									
										14
									
								
								README.md
								
								
								
								
							
							
						
						
									
										14
									
								
								README.md
								
								
								
								
							| 
						 | 
				
			
			@ -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
								
								
								
								
							
							
						
						
									
										576
									
								
								main.ts
								
								
								
								
							| 
						 | 
				
			
			@ -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();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
 | 
			
		||||
*/
 | 
			
		||||
		Loading…
	
		Reference in New Issue