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).
|
An Obsidian plugin for system integration and automation.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## First time developing plugins?
|
## First time developing plugins?
|
||||||
|
|
||||||
|
|
568
main.ts
568
main.ts
|
@ -1,81 +1,77 @@
|
||||||
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 PluginSettings {
|
||||||
|
|
||||||
interface MyPluginSettings {
|
|
||||||
mySetting: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: MyPluginSettings = {
|
const DEFAULT_SETTINGS: PluginSettings = {
|
||||||
mySetting: 'default'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MyPlugin extends Plugin {
|
const FOLDERS = {
|
||||||
settings: MyPluginSettings;
|
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() {
|
async onload() {
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
|
await this.ensureFolderStructure();
|
||||||
|
|
||||||
// This creates an icon in the left ribbon.
|
// Main command for context-aware creation
|
||||||
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
|
this.addCommand({
|
||||||
// Called when the user clicks the icon.
|
id: 'moc-context-create',
|
||||||
new Notice('This is a notice!');
|
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.
|
// Command to duplicate prompt iteration
|
||||||
const statusBarItemEl = this.addStatusBarItem();
|
|
||||||
statusBarItemEl.setText('Status Bar Text');
|
|
||||||
|
|
||||||
// This adds a simple command that can be triggered anywhere
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: 'open-sample-modal-simple',
|
id: 'duplicate-prompt-iteration',
|
||||||
name: 'Open sample modal (simple)',
|
name: 'Duplicate prompt iteration',
|
||||||
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)',
|
|
||||||
checkCallback: (checking: boolean) => {
|
checkCallback: (checking: boolean) => {
|
||||||
// Conditions to check
|
const activeFile = this.app.workspace.getActiveFile();
|
||||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
if (activeFile && this.isPromptIteration(activeFile)) {
|
||||||
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.
|
|
||||||
if (!checking) {
|
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 true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
// Command to open all LLM links
|
||||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
this.addCommand({
|
||||||
|
id: 'open-llm-links',
|
||||||
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
|
name: 'Open all LLM links',
|
||||||
// Using this function will automatically remove the event listener when this plugin is disabled.
|
checkCallback: (checking: boolean) => {
|
||||||
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
|
const activeFile = this.app.workspace.getActiveFile();
|
||||||
console.log('click', evt);
|
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.
|
// Auto-cleanup on file deletion
|
||||||
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
this.registerEvent(
|
||||||
|
this.app.vault.on('delete', (file) => {
|
||||||
|
if (file instanceof TFile) {
|
||||||
|
this.cleanupBrokenLinks(file);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
|
@ -89,16 +85,310 @@ export default class MyPlugin extends Plugin {
|
||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
await this.saveData(this.settings);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SampleModal extends Modal {
|
async handleContextCreate() {
|
||||||
constructor(app: App) {
|
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 CreateMOCModal extends Modal {
|
||||||
|
constructor(app: App, private onSubmit: (name: string) => void) {
|
||||||
super(app);
|
super(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
contentEl.setText('Woah!');
|
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() {
|
onClose() {
|
||||||
|
@ -107,28 +397,152 @@ class SampleModal extends Modal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SampleSettingTab extends PluginSettingTab {
|
class AddToMOCModal extends Modal {
|
||||||
plugin: MyPlugin;
|
constructor(
|
||||||
|
app: App,
|
||||||
constructor(app: App, plugin: MyPlugin) {
|
private moc: TFile,
|
||||||
super(app, plugin);
|
private plugin: MOCSystemPlugin
|
||||||
this.plugin = plugin;
|
) {
|
||||||
|
super(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
display(): void {
|
onOpen() {
|
||||||
const {containerEl} = this;
|
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)
|
options.forEach(option => {
|
||||||
.setName('Setting #1')
|
const button = contentEl.createEl('button', {
|
||||||
.setDesc('It\'s a secret')
|
text: `Create ${option.label}`,
|
||||||
.addText(text => text
|
cls: 'mod-cta'
|
||||||
.setPlaceholder('Enter your secret')
|
});
|
||||||
.setValue(this.plugin.settings.mySetting)
|
button.style.display = 'block';
|
||||||
.onChange(async (value) => {
|
button.style.width = '100%';
|
||||||
this.plugin.settings.mySetting = value;
|
button.style.marginBottom = '10px';
|
||||||
await this.plugin.saveSettings();
|
|
||||||
}));
|
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",
|
"id": "my-obsidian-system-plugin",
|
||||||
"name": "Sample Plugin",
|
"name": "MOC System Plugin",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "Demonstrates some of the capabilities of the Obsidian API.",
|
"description": "Automated MOC-based note management system",
|
||||||
"author": "Obsidian",
|
"author": "Your Name",
|
||||||
"authorUrl": "https://obsidian.md",
|
"authorUrl": "",
|
||||||
"fundingUrl": "https://obsidian.md/pricing",
|
|
||||||
"isDesktopOnly": false
|
"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",
|
"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",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node esbuild.config.mjs",
|
"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