Reformat files with tools

This commit is contained in:
Mark Ayers 2025-03-13 00:44:01 -04:00
parent f7bae47850
commit f0c22d65b0
7 changed files with 389 additions and 331 deletions

View file

@ -1,35 +1,35 @@
name: Release Obsidian plugin
on:
push:
tags:
- "*"
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
- name: Build plugin
run: |
npm install
npm run build
- name: Build plugin
run: |
npm install
npm run build
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
gh release create "$tag" \
--title="$tag" \
--draft \
main.js manifest.json styles.css
gh release create "$tag" \
--title="$tag" \
--draft \
main.js manifest.json styles.css

View file

@ -5,19 +5,24 @@
This plugin helps break down large notes into smaller, "atomic" style notes and ideas, using the OpenAI API.
## Requirements
- An OpenAI API key is required for full functionality
## Usage
- Set your OpenAI API key in settings.
- Open the command palette and select "Atomize Selected Note" to process the currently selected note; Alternatively, you can use the icon in the left sidebar.
- Sends selected note to OpenAI for processing, where it will be broken down into "atomic notes" that aim to extract key insights from the original note.
- Frontmatter will be added to each new note, including the date and any custom tags you've configured from the settings menu.
### Models supported
- `gpt-4o-mini`
- `gpt-4o`
## Network Usage Disclosure
This plugin makes network requests to:
- OpenAI API (api.openai.com) for generating atomic notes from your content
- Your notes' content is sent to OpenAI for processing. Please review OpenAI's privacy policy.

View file

@ -2,14 +2,13 @@ import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const prod = process.argv[2] === "production";
const context = await esbuild.context({
banner: {
@ -31,7 +30,8 @@ const context = await esbuild.context({
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
...builtins,
],
format: "cjs",
target: "es2018",
logLevel: "info",

View file

@ -3,12 +3,11 @@ import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
/** @type {import('eslint').Linter.Config[]} */
export default [
{files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"]},
{languageOptions: { globals: globals.browser }},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
];
{ files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] },
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
];

View file

@ -2,26 +2,26 @@ import globals from "globals";
import tseslint from "typescript-eslint";
export default [
{
files: ["**/*.{js,mjs,cjs,ts}"],
ignores: ["node_modules/**", "main.js"],
languageOptions: {
globals: globals.browser,
parser: tseslint.parser,
parserOptions: {
project: "./tsconfig.json"
}
},
plugins: {
"@typescript-eslint": tseslint.plugin
},
rules: {
...tseslint.configs.recommended.rules,
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}
];
{
files: ["**/*.{js,mjs,cjs,ts}"],
ignores: ["node_modules/**", "main.js"],
languageOptions: {
globals: globals.browser,
parser: tseslint.parser,
parserOptions: {
project: "./tsconfig.json",
},
},
plugins: {
"@typescript-eslint": tseslint.plugin,
},
rules: {
...tseslint.configs.recommended.rules,
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off",
},
},
];

557
main.ts
View file

@ -1,47 +1,58 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, normalizePath } from 'obsidian';
import OpenAI from 'openai';
import {
App,
Editor,
MarkdownView,
Modal,
Notice,
Plugin,
PluginSettingTab,
Setting,
normalizePath,
} from "obsidian";
import OpenAI from "openai";
// Types and Interfaces
interface APIError extends Error {
response?: {
status: number;
data: any;
};
response?: {
status: number;
data: any;
};
}
interface AtomicPluginSettings {
apiKey: string;
outputFolder: string;
model: string;
enableAtomizedTag: boolean;
customTags: string;
apiKey: string;
outputFolder: string;
model: string;
enableAtomizedTag: boolean;
customTags: string;
}
// Constants
const DEFAULT_SETTINGS: AtomicPluginSettings = {
apiKey: '',
outputFolder: 'atomic-notes',
model: 'gpt-4o-mini',
enableAtomizedTag: true,
customTags: ''
}
apiKey: "",
outputFolder: "atomic-notes",
model: "gpt-4o-mini",
enableAtomizedTag: true,
customTags: "",
};
const getCurrentDateTime = (): string => {
const date = new Date();
return date.toISOString()
.replace('T', ' ')
.replace(/\.\d+Z$/, '');
const date = new Date();
return date
.toISOString()
.replace("T", " ")
.replace(/\.\d+Z$/, "");
};
// Move SYSTEM_PROMPT into a function that takes settings as a parameter
const getSystemPrompt = (settings: AtomicPluginSettings): string => {
return `You are an expert at creating atomic notes from a single, larger note.
return `You are an expert at creating atomic notes from a single, larger note.
Take the content from a larger note and break it down into separate compact yet detailed atomic notes. Each note MUST be separated by placing '<<<>>>' on its own line between notes. Do not include an index or main note. Follow these rules:
1. Each note should contain exactly one clear idea. This can contain multiple lines.
2. Each note must have a YAML frontmatter section at the top with:
---
date: "${getCurrentDateTime()}"
tags: ${settings.enableAtomizedTag ? 'atomized' : ''}${settings.customTags ? (settings.enableAtomizedTag ? ', ' : '') + settings.customTags : ''}
tags: ${settings.enableAtomizedTag ? "atomized" : ""}${settings.customTags ? (settings.enableAtomizedTag ? ", " : "") + settings.customTags : ""}
---
3. You MUST separate each note by placing '<<<>>>' on its own line between notes
4. After the frontmatter, each note must start with a level 1 heading (# Title)
@ -52,282 +63,332 @@ tags: ${settings.enableAtomizedTag ? 'atomized' : ''}${settings.customTags ? (se
// OpenAI Service
class OpenAIService {
constructor(private apiKey: string, private model: string, private settings: AtomicPluginSettings) {}
constructor(
private apiKey: string,
private model: string,
private settings: AtomicPluginSettings,
) {}
async generateAtomicNotes(content: string, timestamp: string): Promise<string> {
if (!this.apiKey) {
new Notice('OpenAI API key is not set. Please configure it in plugin settings.');
return '';
}
async generateAtomicNotes(
content: string,
timestamp: string,
): Promise<string> {
if (!this.apiKey) {
new Notice(
"OpenAI API key is not set. Please configure it in plugin settings.",
);
return "";
}
// Inform user about network request
new Notice('Sending request to OpenAI...', 3000);
// Inform user about network request
new Notice("Sending request to OpenAI...", 3000);
const openai = new OpenAI({
apiKey: this.apiKey,
dangerouslyAllowBrowser: true
});
const openai = new OpenAI({
apiKey: this.apiKey,
dangerouslyAllowBrowser: true,
});
const completion = await openai.chat.completions.create({
model: this.model,
messages: [{
role: "system",
content: getSystemPrompt(this.settings)
}, {
role: "user",
content: content
}],
temperature: 0.7,
max_tokens: 4000
});
const completion = await openai.chat.completions.create({
model: this.model,
messages: [
{
role: "system",
content: getSystemPrompt(this.settings),
},
{
role: "user",
content: content,
},
],
temperature: 0.7,
max_tokens: 4000,
});
return completion.choices[0]?.message?.content ?? '';
}
return completion.choices[0]?.message?.content ?? "";
}
}
// Notes Manager
class NotesManager {
private usedTitles = new Set<string>();
private usedTitles = new Set<string>();
constructor(private app: App, private folderPath: string) {}
constructor(
private app: App,
private folderPath: string,
) {}
async createFolder(): Promise<void> {
if (!(await this.app.vault.adapter.exists(this.folderPath))) {
await this.app.vault.createFolder(this.folderPath);
}
}
async createFolder(): Promise<void> {
if (!(await this.app.vault.adapter.exists(this.folderPath))) {
await this.app.vault.createFolder(this.folderPath);
}
}
async saveNote(note: string): Promise<void> {
const titleMatch = note.match(/^#\s+(.+)$/m);
if (!titleMatch) {
console.warn('No title found in note:', note.slice(0, 100));
return;
}
async saveNote(note: string): Promise<void> {
const titleMatch = note.match(/^#\s+(.+)$/m);
if (!titleMatch) {
console.warn("No title found in note:", note.slice(0, 100));
return;
}
const title = this.getUniqueTitle(titleMatch[1].trim());
const fileName = `${this.folderPath}/${normalizePath(title)}.md`;
await this.app.vault.create(fileName, note.trim());
}
const title = this.getUniqueTitle(titleMatch[1].trim());
const fileName = `${this.folderPath}/${normalizePath(title)}.md`;
await this.app.vault.create(fileName, note.trim());
}
private getUniqueTitle(baseTitle: string): string {
let title = baseTitle;
let counter = 1;
while (this.usedTitles.has(title)) {
title = `${baseTitle} ${counter}`;
counter++;
}
return normalizePath(title);
}
private getUniqueTitle(baseTitle: string): string {
let title = baseTitle;
let counter = 1;
while (this.usedTitles.has(title)) {
title = `${baseTitle} ${counter}`;
counter++;
}
return normalizePath(title);
}
}
// Main Plugin Class
export default class AtomicNotesPlugin extends Plugin {
settings: AtomicPluginSettings;
private statusBarItem: HTMLElement;
settings: AtomicPluginSettings;
private statusBarItem: HTMLElement;
async onload() {
await this.loadSettings();
async onload() {
await this.loadSettings();
// Add ribbon icon
this.addRibbonIcon('atom', 'Atomize note', async () => {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeView) {
await this.atomizeNote(activeView);
} else {
new Notice('Please open a note to atomize');
}
});
// Add ribbon icon
this.addRibbonIcon("atom", "Atomize note", async () => {
const activeView =
this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeView) {
await this.atomizeNote(activeView);
} else {
new Notice("Please open a note to atomize");
}
});
// Add command
this.addCommand({
id: 'atomize-current-note',
name: 'Atomize current note',
editorCallback: async (editor: Editor, view: MarkdownView) => {
await this.atomizeNote(view);
}
});
// Add command
this.addCommand({
id: "atomize-current-note",
name: "Atomize current note",
editorCallback: async (editor: Editor, view: MarkdownView) => {
await this.atomizeNote(view);
},
});
// Add settings tab
this.addSettingTab(new AtomicSettingTab(this.app, this));
// Add settings tab
this.addSettingTab(new AtomicSettingTab(this.app, this));
this.statusBarItem = this.addStatusBarItem();
this.statusBarItem.setText('Atomic notes: ready');
}
this.statusBarItem = this.addStatusBarItem();
this.statusBarItem.setText("Atomic notes: ready");
}
async atomizeNote(view: MarkdownView) {
if (!this.validateSettings()) return;
async atomizeNote(view: MarkdownView) {
if (!this.validateSettings()) return;
const content = view.getViewData();
if (!content.trim()) {
new Notice('The current note is empty');
return;
}
const content = view.getViewData();
if (!content.trim()) {
new Notice("The current note is empty");
return;
}
new ConfirmationModal(this.app, async () => {
const loadingNotice = new Notice('Processing note...', 0);
new ConfirmationModal(this.app, async () => {
const loadingNotice = new Notice("Processing note...", 0);
try {
const openAIService = new OpenAIService(
this.settings.apiKey,
this.settings.model,
this.settings
);
const notesManager = new NotesManager(this.app, this.settings.outputFolder);
try {
const openAIService = new OpenAIService(
this.settings.apiKey,
this.settings.model,
this.settings,
);
const notesManager = new NotesManager(
this.app,
this.settings.outputFolder,
);
const responseContent = await openAIService.generateAtomicNotes(content, this.getCurrentDateTime());
if (!responseContent) throw new Error('No content received from OpenAI');
const responseContent = await openAIService.generateAtomicNotes(
content,
this.getCurrentDateTime(),
);
if (!responseContent)
throw new Error("No content received from OpenAI");
const atomicNotes = responseContent
.split('<<<>>>')
.map(note => note.trim())
.filter(note => note.length > 0);
const atomicNotes = responseContent
.split("<<<>>>")
.map((note) => note.trim())
.filter((note) => note.length > 0);
if (atomicNotes.length === 0) {
throw new Error('No atomic notes were generated. The response may not be properly formatted.');
}
if (atomicNotes.length === 0) {
throw new Error(
"No atomic notes were generated. The response may not be properly formatted.",
);
}
await notesManager.createFolder();
await notesManager.createFolder();
for (const note of atomicNotes) {
await notesManager.saveNote(note);
}
for (const note of atomicNotes) {
await notesManager.saveNote(note);
}
new Notice(`Successfully created ${atomicNotes.length} atomic notes`);
} catch (error) {
this.handleError(error);
} finally {
loadingNotice.hide();
this.statusBarItem.setText('Atomic Notes: Ready');
}
}).open();
}
new Notice(
`Successfully created ${atomicNotes.length} atomic notes`,
);
} catch (error) {
this.handleError(error);
} finally {
loadingNotice.hide();
this.statusBarItem.setText("Atomic Notes: Ready");
}
}).open();
}
private validateSettings(): boolean {
if (!this.settings.apiKey) {
new Notice('Please set your OpenAI API key in settings');
return false;
}
return true;
}
private validateSettings(): boolean {
if (!this.settings.apiKey) {
new Notice("Please set your OpenAI API key in settings");
return false;
}
return true;
}
private handleError(error: APIError) {
console.error('Detailed error:', error);
new Notice(`Error: ${error.message || 'Unknown error occurred'}`);
if (error.response) {
console.error('OpenAI API Error:', {
status: error.response.status,
data: error.response.data
});
}
}
private handleError(error: APIError) {
console.error("Detailed error:", error);
new Notice(`Error: ${error.message || "Unknown error occurred"}`);
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
if (error.response) {
console.error("OpenAI API Error:", {
status: error.response.status,
data: error.response.data,
});
}
}
async saveSettings() {
await this.saveData(this.settings);
}
async loadSettings() {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData(),
);
}
private getCurrentDateTime(): string {
return new Date().toISOString();
}
async saveSettings() {
await this.saveData(this.settings);
}
private getCurrentDateTime(): string {
return new Date().toISOString();
}
}
class AtomicSettingTab extends PluginSettingTab {
plugin: AtomicNotesPlugin;
plugin: AtomicNotesPlugin;
constructor(app: App, plugin: AtomicNotesPlugin) {
super(app, plugin);
this.plugin = plugin;
}
constructor(app: App, plugin: AtomicNotesPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName('OpenAI API key')
.setDesc('Enter your OpenAI API key')
.addText(text => text
.setPlaceholder('sk-...')
.setValue(this.plugin.settings.apiKey)
.onChange(async (value) => {
this.plugin.settings.apiKey = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName("OpenAI API key")
.setDesc("Enter your OpenAI API key")
.addText((text) =>
text
.setPlaceholder("sk-...")
.setValue(this.plugin.settings.apiKey)
.onChange(async (value) => {
this.plugin.settings.apiKey = value;
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName('Model')
.setDesc('Choose the OpenAI model to use')
.addDropdown(dropdown => dropdown
.addOption('gpt-4o-mini', 'GPT-4o Mini')
.addOption('gpt-4o', 'GPT-4o')
.setValue(this.plugin.settings.model)
.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName("Model")
.setDesc("Choose the OpenAI model to use")
.addDropdown((dropdown) =>
dropdown
.addOption("gpt-4o-mini", "GPT-4o Mini")
.addOption("gpt-4o", "GPT-4o")
.setValue(this.plugin.settings.model)
.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName('Output folder')
.setDesc('Folder where atomic notes will be created')
.addText(text => text
.setPlaceholder('atomic-notes')
.setValue(this.plugin.settings.outputFolder)
.onChange(async (value) => {
this.plugin.settings.outputFolder = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName("Output folder")
.setDesc("Folder where atomic notes will be created")
.addText((text) =>
text
.setPlaceholder("atomic-notes")
.setValue(this.plugin.settings.outputFolder)
.onChange(async (value) => {
this.plugin.settings.outputFolder = value;
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName('Enable "atomized" tag')
.setDesc('Automatically add the "atomized" tag to generated notes')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableAtomizedTag)
.onChange(async (value) => {
this.plugin.settings.enableAtomizedTag = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Enable "atomized" tag')
.setDesc('Automatically add the "atomized" tag to generated notes')
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.enableAtomizedTag)
.onChange(async (value) => {
this.plugin.settings.enableAtomizedTag = value;
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName('Custom tags')
.setDesc('Additional tags to add to generated notes (comma-separated)')
.addText(text => text
.setPlaceholder('tag1, tag2, tag3')
.setValue(this.plugin.settings.customTags)
.onChange(async (value) => {
this.plugin.settings.customTags = value;
await this.plugin.saveSettings();
}));
}
new Setting(containerEl)
.setName("Custom tags")
.setDesc(
"Additional tags to add to generated notes (comma-separated)",
)
.addText((text) =>
text
.setPlaceholder("tag1, tag2, tag3")
.setValue(this.plugin.settings.customTags)
.onChange(async (value) => {
this.plugin.settings.customTags = value;
await this.plugin.saveSettings();
}),
);
}
}
class ConfirmationModal extends Modal {
constructor(app: App, private onConfirm: () => void) {
super(app);
}
constructor(
app: App,
private onConfirm: () => void,
) {
super(app);
}
onOpen() {
const {contentEl} = this;
contentEl.setText('This will create multiple atomic notes in your vault. Continue?');
new Setting(contentEl)
.addButton(btn => btn
.setButtonText('Cancel')
.onClick(() => this.close()))
.addButton(btn => btn
.setButtonText('Continue')
.setCta()
.onClick(() => {
this.close();
this.onConfirm();
}));
}
onOpen() {
const { contentEl } = this;
contentEl.setText(
"This will create multiple atomic notes in your vault. Continue?",
);
onClose() {
const {contentEl} = this;
contentEl.empty();
}
new Setting(contentEl)
.addButton((btn) =>
btn.setButtonText("Cancel").onClick(() => this.close()),
)
.addButton((btn) =>
btn
.setButtonText("Continue")
.setCta()
.onClick(() => {
this.close();
this.onConfirm();
}),
);
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

View file

@ -1,24 +1,17 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"**/*.ts"
]
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": ["DOM", "ES5", "ES6", "ES7"]
},
"include": ["**/*.ts"]
}