mirror of
https://github.com/bitsofchris/openaugi-obsidian-plugin.git
synced 2026-07-22 12:40:27 +00:00
Add TaskFileService, which writes `status: pending` task files to
OpenAugi/Tasks/ via the Obsidian vault API — no shell-out, no HTTP, no
Node modules, so it works on mobile too. The file format mirrors the
parent repo's templates/task-template.md contract consumed by
task_watcher.py.
New commands (all platforms):
- Augi: Run review pass -> "run the review pass"
- Augi: Process dashboard -> "process the dashboard"
- Augi: Distill selection -> selection/active-note body becomes the
## Context; distill-lens instruction
Deprecate the legacy Task Dispatch feature (it launches tmux itself,
bypassing the watcher). Kept functional for community users without the
Python watcher; steer new use to task files via docs banner, settings UI
label, and a @deprecated marker. To be removed over a release or two.
Docs: new docs/AGENT_TASKS.md; README + CODEBASE_MAP updated to lead with
the task-file flow. Tests: 18 new, including a watcher-frontmatter-regex
compat check and same-second collision handling.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
839 lines
No EOL
27 KiB
TypeScript
839 lines
No EOL
27 KiB
TypeScript
import { Plugin, Notice, TFile, Platform } from 'obsidian';
|
|
import { OpenAugiSettings, DEFAULT_SETTINGS } from './types/settings';
|
|
import { OpenAIService } from './services/openai-service';
|
|
import { FileService } from './services/file-service';
|
|
import { DistillService } from './services/distill-service';
|
|
import { ContextGatheringService } from './services/context-gathering-service';
|
|
import { TaskFileService, stripFrontmatter } from './services/task-file-service';
|
|
// Lazy-imported: TaskDispatchService uses Node.js modules (child_process, fs, path)
|
|
// that are unavailable on mobile. We dynamic-import it only on desktop.
|
|
import type { TaskDispatchService } from './services/task-dispatch-service';
|
|
import { OpenAugiSettingTab } from './ui/settings-tab';
|
|
import { LoadingIndicator } from './ui/loading-indicator';
|
|
import { SessionListModal } from './ui/session-list-modal';
|
|
import { sanitizeFilename, createFileWithCollisionHandling } from './utils/filename-utils';
|
|
import { PromptSelectionModal, PromptSelectionConfig } from './ui/prompt-selection-modal';
|
|
import { ContextGatheringModal } from './ui/context-gathering-modal';
|
|
import { ContextSelectionModal } from './ui/context-selection-modal';
|
|
import { ContextPreviewModal } from './ui/context-preview-modal';
|
|
import { CommandOptions, ContextGatheringConfig, GatheredContext, DiscoveredNote } from './types/context';
|
|
import { TaskSession } from './types/task-dispatch';
|
|
|
|
/**
|
|
* A simple tokeinzer to estimate the number of tokens
|
|
* @param text Text to count tokens from
|
|
* @returns Approximate token count
|
|
*/
|
|
function estimateTokens(text: string): number {
|
|
// Rough estimate: 1 token is approximately 4 characters
|
|
return Math.ceil(text.length / 4);
|
|
}
|
|
|
|
export default class OpenAugiPlugin extends Plugin {
|
|
settings: OpenAugiSettings;
|
|
openAIService: OpenAIService;
|
|
fileService: FileService;
|
|
distillService: DistillService;
|
|
contextGatheringService: ContextGatheringService;
|
|
taskFileService: TaskFileService;
|
|
taskDispatchService: TaskDispatchService | null;
|
|
loadingIndicator: LoadingIndicator;
|
|
|
|
async onload() {
|
|
// Load settings
|
|
await this.loadSettings();
|
|
|
|
// Initialize services
|
|
this.initializeServices();
|
|
|
|
// Initialize loading indicator
|
|
this.app.workspace.onLayoutReady(() => {
|
|
const statusBar = this.addStatusBarItem();
|
|
if (statusBar.parentElement) {
|
|
this.loadingIndicator = new LoadingIndicator(statusBar.parentElement);
|
|
}
|
|
});
|
|
|
|
// Add command to manually parse a transcript file
|
|
this.addCommand({
|
|
id: 'parse-transcript',
|
|
name: 'Parse transcript',
|
|
callback: async () => {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (activeFile && activeFile.extension === 'md') {
|
|
await this.processTranscriptFile(activeFile);
|
|
} else {
|
|
new Notice('Please open a markdown transcript file first');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add command to distill linked notes
|
|
this.addCommand({
|
|
id: 'distill-notes',
|
|
name: 'Distill linked notes',
|
|
callback: async () => {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (activeFile && activeFile.extension === 'md') {
|
|
await this.distillLinkedNotes(activeFile);
|
|
} else {
|
|
new Notice('Please open a markdown file first');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add new unified context gathering commands
|
|
this.addCommand({
|
|
id: 'openaugi-process-notes',
|
|
name: 'Process notes',
|
|
callback: async () => {
|
|
await this.gatherAndProcessContext({
|
|
commandType: 'distill',
|
|
defaultSourceMode: 'linked-notes',
|
|
defaultDepth: 1
|
|
});
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'openaugi-process-recent',
|
|
name: 'Process recent activity',
|
|
callback: async () => {
|
|
await this.gatherAndProcessContext({
|
|
commandType: 'distill',
|
|
defaultSourceMode: 'recent-activity',
|
|
defaultDepth: 1
|
|
});
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'openaugi-save-context',
|
|
name: 'Save context',
|
|
callback: async () => {
|
|
await this.gatherAndProcessContext({
|
|
commandType: 'save-raw',
|
|
defaultSourceMode: 'linked-notes',
|
|
defaultDepth: 1,
|
|
skipPreview: false
|
|
});
|
|
}
|
|
});
|
|
|
|
// Augi task-file commands — write a pending task to OpenAugi/Tasks/
|
|
// for the OpenAugi task watcher to pick up. Pure vault API, works on
|
|
// mobile too. These supersede the deprecated Task Dispatch commands.
|
|
this.addCommand({
|
|
id: 'augi-run-review-pass',
|
|
name: 'Augi: Run review pass',
|
|
callback: async () => {
|
|
await this.queueTaskFile(() => this.taskFileService.createReviewPassTask());
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'augi-process-dashboard',
|
|
name: 'Augi: Process dashboard',
|
|
callback: async () => {
|
|
await this.queueTaskFile(() => this.taskFileService.createProcessDashboardTask());
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'augi-distill-selection',
|
|
name: 'Augi: Distill selection',
|
|
callback: async () => {
|
|
await this.distillSelection();
|
|
}
|
|
});
|
|
|
|
// Task dispatch commands — desktop only.
|
|
// DEPRECATED: these launch tmux directly from the plugin, bypassing the
|
|
// task watcher. Kept working for existing users; will be removed over a
|
|
// release or two. New workflows should use the Augi commands above.
|
|
if (!Platform.isMobile) {
|
|
this.addCommand({
|
|
id: 'task-dispatch-launch',
|
|
name: 'Task dispatch: Launch or attach',
|
|
callback: async () => {
|
|
if (!this.taskDispatchService) {
|
|
new Notice('Task dispatch is still loading, try again in a moment');
|
|
return;
|
|
}
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (activeFile && activeFile.extension === 'md') {
|
|
await this.taskDispatchService.launchOrAttach(activeFile);
|
|
} else {
|
|
new Notice('Please open a task note first');
|
|
}
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'task-dispatch-kill',
|
|
name: 'Task dispatch: Kill session',
|
|
callback: async () => {
|
|
if (!this.taskDispatchService) {
|
|
new Notice('Task dispatch is still loading, try again in a moment');
|
|
return;
|
|
}
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (activeFile && activeFile.extension === 'md') {
|
|
await this.taskDispatchService.killSession(activeFile);
|
|
} else {
|
|
new Notice('Please open a task note first');
|
|
}
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'task-dispatch-list',
|
|
name: 'Task dispatch: List active sessions',
|
|
callback: async () => {
|
|
if (!this.taskDispatchService) {
|
|
new Notice('Task dispatch is still loading, try again in a moment');
|
|
return;
|
|
}
|
|
const sessions = await this.taskDispatchService.listActiveSessions();
|
|
const modal = new SessionListModal(
|
|
this.app,
|
|
sessions,
|
|
async (session: TaskSession) => {
|
|
await this.taskDispatchService!.openTerminal(session.tmuxSessionName);
|
|
},
|
|
async (session: TaskSession) => {
|
|
await this.taskDispatchService!.killSessionById(session.taskId);
|
|
new Notice(`Killed session: ${session.taskId}`);
|
|
}
|
|
);
|
|
modal.open();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add settings tab
|
|
this.addSettingTab(new OpenAugiSettingTab(this.app, this));
|
|
}
|
|
|
|
/**
|
|
* Get the configured model (custom override or default)
|
|
*/
|
|
private getConfiguredModel(): string {
|
|
return this.settings.customModelOverride.trim() || this.settings.defaultModel;
|
|
}
|
|
|
|
/**
|
|
* Initialize services with current settings
|
|
*/
|
|
private initializeServices(): void {
|
|
this.openAIService = new OpenAIService(this.settings.apiKey, this.getConfiguredModel());
|
|
this.fileService = new FileService(
|
|
this.app,
|
|
this.settings.summaryFolder,
|
|
this.settings.notesFolder,
|
|
this.settings.publishedFolder
|
|
);
|
|
this.distillService = new DistillService(
|
|
this.app,
|
|
this.openAIService,
|
|
this.settings
|
|
);
|
|
this.contextGatheringService = new ContextGatheringService(
|
|
this.app,
|
|
this.distillService,
|
|
this.settings
|
|
);
|
|
this.taskFileService = new TaskFileService(this.app);
|
|
|
|
// TaskDispatchService uses Node.js modules (child_process, fs, path)
|
|
// unavailable on mobile. Skip entirely on mobile.
|
|
this.taskDispatchService = null;
|
|
if (!Platform.isMobile) {
|
|
import('./services/task-dispatch-service')
|
|
.then(({ TaskDispatchService }) => {
|
|
this.taskDispatchService = new TaskDispatchService(
|
|
this.app,
|
|
this.settings,
|
|
this.distillService
|
|
);
|
|
})
|
|
.catch(() => {
|
|
// Fallback: Node.js modules unavailable in this environment.
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run a task-file writer and surface the result as a Notice.
|
|
* @param write Callback that writes the task file and returns its path
|
|
*/
|
|
private async queueTaskFile(write: () => Promise<string>): Promise<void> {
|
|
try {
|
|
const path = await write();
|
|
const filename = path.split('/').pop();
|
|
new Notice(`Task queued: ${filename}\nThe OpenAugi task watcher will pick it up.`);
|
|
} catch (error) {
|
|
console.error('Failed to write task file:', error);
|
|
new Notice('Failed to write task file. Check console for details.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Queue a distill task for the current selection, or the active note's
|
|
* body when nothing is selected. The chosen text becomes the task's
|
|
* ## Context section — the plugin acts as the scope selector.
|
|
*/
|
|
private async distillSelection(): Promise<void> {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
const selection = this.app.workspace.activeEditor?.editor?.getSelection()?.trim() ?? '';
|
|
|
|
let context = selection;
|
|
if (!context) {
|
|
if (!activeFile || activeFile.extension !== 'md') {
|
|
new Notice('Select text or open a markdown note to distill');
|
|
return;
|
|
}
|
|
const raw = await this.app.vault.read(activeFile);
|
|
context = stripFrontmatter(raw).trim();
|
|
}
|
|
|
|
if (!context) {
|
|
new Notice('Nothing to distill — the note is empty');
|
|
return;
|
|
}
|
|
|
|
const sourceNote = activeFile?.basename ?? 'OpenAugi plugin';
|
|
await this.queueTaskFile(() => this.taskFileService.createDistillTask(context, sourceNote));
|
|
}
|
|
|
|
/**
|
|
* Open a file in a new tab
|
|
* @param filePath The path to the file to open
|
|
*/
|
|
private async openFileInNewTab(filePath: string): Promise<void> {
|
|
const file = this.app.vault.getAbstractFileByPath(filePath);
|
|
if (!file || !(file instanceof TFile)) {
|
|
console.error(`File not found: ${filePath}`);
|
|
return;
|
|
}
|
|
|
|
const leaf = this.app.workspace.getLeaf('tab');
|
|
await leaf.openFile(file);
|
|
}
|
|
|
|
/**
|
|
* Process a transcript file
|
|
* @param file The file to process
|
|
*/
|
|
private async processTranscriptFile(file: TFile): Promise<void> {
|
|
try {
|
|
// Show loading indicator
|
|
this.loadingIndicator?.show('Processing voice transcript');
|
|
|
|
// Read file content
|
|
const content = await this.app.vault.read(file);
|
|
|
|
// Check if API key is set
|
|
if (!this.settings.apiKey) {
|
|
this.loadingIndicator?.hide();
|
|
new Notice('Please set your OpenAI API key in the plugin settings');
|
|
return;
|
|
}
|
|
|
|
// Display character and token count
|
|
new Notice(`Processing transcript: ${file.basename}\nCharacters: ${content.length}\nEst. Tokens: ${estimateTokens(content)}`);
|
|
|
|
// Update openAIService with latest API key and model
|
|
this.openAIService = new OpenAIService(this.settings.apiKey, this.getConfiguredModel());
|
|
|
|
// Parse transcript
|
|
const parsedData = await this.openAIService.parseTranscript(content);
|
|
|
|
// Write result to files
|
|
await this.fileService.writeTranscriptFiles(file.basename, parsedData);
|
|
|
|
// Hide loading indicator
|
|
this.loadingIndicator?.hide();
|
|
|
|
// Show success message
|
|
new Notice(`Successfully parsed transcript: ${file.basename}\nCreated ${parsedData.notes.length} atomic notes`);
|
|
|
|
// Open the summary file in a new tab
|
|
const sanitizedFilename = sanitizeFilename(file.basename);
|
|
const summaryPath = `${this.settings.summaryFolder}/${sanitizedFilename} - summary.md`;
|
|
await this.openFileInNewTab(summaryPath);
|
|
} catch (error) {
|
|
// Hide loading indicator
|
|
this.loadingIndicator?.hide();
|
|
|
|
console.error('Failed to parse transcript:', error);
|
|
new Notice('Failed to parse transcript. Check console for details.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Distill linked notes
|
|
* @param rootFile The root file containing links to distill
|
|
*/
|
|
private async distillLinkedNotes(rootFile: TFile): Promise<void> {
|
|
// Show prompt selection modal
|
|
const modal = new PromptSelectionModal(
|
|
this.app,
|
|
this.settings.promptsFolder,
|
|
async (config: PromptSelectionConfig) => {
|
|
await this.executeDistillLinkedNotes(rootFile, config);
|
|
}
|
|
);
|
|
modal.open();
|
|
}
|
|
|
|
/**
|
|
* Execute the distillation of linked notes with the selected prompt configuration
|
|
* @param rootFile The root note file
|
|
* @param promptConfig The prompt configuration from the modal
|
|
*/
|
|
private async executeDistillLinkedNotes(rootFile: TFile, promptConfig: PromptSelectionConfig): Promise<void> {
|
|
try {
|
|
// Show loading indicator
|
|
this.loadingIndicator?.show('Distilling linked notes');
|
|
|
|
// Check if API key is set
|
|
if (!this.settings.apiKey) {
|
|
this.loadingIndicator?.hide();
|
|
new Notice('Please set your OpenAI API key in the plugin settings');
|
|
return;
|
|
}
|
|
|
|
// Update services with latest API key and model
|
|
this.openAIService = new OpenAIService(this.settings.apiKey, this.getConfiguredModel());
|
|
this.distillService = new DistillService(
|
|
this.app,
|
|
this.openAIService,
|
|
this.settings
|
|
);
|
|
|
|
// Get linked files
|
|
let linkedFiles = await this.distillService.getLinkedNotes(rootFile);
|
|
|
|
// Deduplicate the linked files by path
|
|
const uniqueFiles = new Map<string, TFile>();
|
|
for (const file of linkedFiles) {
|
|
if (!uniqueFiles.has(file.path)) {
|
|
uniqueFiles.set(file.path, file);
|
|
}
|
|
}
|
|
|
|
// Convert back to array
|
|
linkedFiles = Array.from(uniqueFiles.values());
|
|
|
|
if (linkedFiles.length === 0) {
|
|
this.loadingIndicator?.hide();
|
|
new Notice('No linked notes found to distill');
|
|
return;
|
|
}
|
|
|
|
// Show notice about linked files
|
|
new Notice(`Found ${linkedFiles.length} linked notes to process.`);
|
|
|
|
// Read custom prompt if selected
|
|
let customPrompt: string | undefined;
|
|
if (promptConfig.useCustomPrompt && promptConfig.selectedPrompt) {
|
|
try {
|
|
customPrompt = await this.app.vault.read(promptConfig.selectedPrompt);
|
|
} catch (error) {
|
|
console.error('Failed to read custom prompt:', error);
|
|
new Notice('Failed to read custom prompt, using default');
|
|
}
|
|
}
|
|
|
|
// Let the distill service handle all the content aggregation (no time filtering)
|
|
const distilledData = await this.distillService.distillFromRootNote(
|
|
rootFile,
|
|
undefined,
|
|
undefined,
|
|
0, // No time filtering for regular distill command
|
|
customPrompt
|
|
);
|
|
|
|
// Write result to files
|
|
const summaryPath = await this.fileService.writeDistilledFiles(rootFile, distilledData);
|
|
|
|
// Hide loading indicator
|
|
this.loadingIndicator?.hide();
|
|
|
|
// Show success message
|
|
new Notice(`Successfully distilled notes from: ${rootFile.basename}\nCreated ${distilledData.notes.length} atomic notes`);
|
|
|
|
// Open the summary file in a new tab
|
|
await this.openFileInNewTab(summaryPath);
|
|
} catch (error) {
|
|
// Hide loading indicator
|
|
this.loadingIndicator?.hide();
|
|
|
|
console.error('Failed to distill notes:', error);
|
|
new Notice('Failed to distill notes. Check console for details.');
|
|
}
|
|
}
|
|
|
|
async loadSettings() {
|
|
const savedData = await this.loadData();
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, savedData);
|
|
|
|
// Deep merge nested settings to pick up new defaults
|
|
if (savedData?.contextGatheringDefaults) {
|
|
this.settings.contextGatheringDefaults = Object.assign(
|
|
{},
|
|
DEFAULT_SETTINGS.contextGatheringDefaults,
|
|
savedData.contextGatheringDefaults
|
|
);
|
|
}
|
|
if (savedData?.recentActivityDefaults) {
|
|
this.settings.recentActivityDefaults = Object.assign(
|
|
{},
|
|
DEFAULT_SETTINGS.recentActivityDefaults,
|
|
savedData.recentActivityDefaults
|
|
);
|
|
}
|
|
if (savedData?.taskDispatch) {
|
|
this.settings.taskDispatch = Object.assign(
|
|
{},
|
|
DEFAULT_SETTINGS.taskDispatch,
|
|
savedData.taskDispatch
|
|
);
|
|
}
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
// Reinitialize services with new settings
|
|
this.initializeServices();
|
|
}
|
|
|
|
|
|
/**
|
|
* Main orchestration method for unified context gathering and processing
|
|
* @param options Options specifying the command type and defaults
|
|
*/
|
|
private async gatherAndProcessContext(options: CommandOptions): Promise<void> {
|
|
// Step 1: Show context gathering configuration modal
|
|
const configModal = new ContextGatheringModal(
|
|
this.app,
|
|
this.settings,
|
|
this.contextGatheringService,
|
|
async (config: ContextGatheringConfig) => {
|
|
await this.executeContextGathering(config, options);
|
|
},
|
|
options.defaultSourceMode,
|
|
options.defaultDepth
|
|
);
|
|
configModal.open();
|
|
}
|
|
|
|
/**
|
|
* Execute context gathering with the given configuration
|
|
* @param config Context gathering configuration
|
|
* @param options Command options
|
|
*/
|
|
private async executeContextGathering(
|
|
config: ContextGatheringConfig,
|
|
options: CommandOptions
|
|
): Promise<void> {
|
|
try {
|
|
this.loadingIndicator?.show('Discovering notes...');
|
|
|
|
// Gather context
|
|
const gatheredContext = await this.contextGatheringService.gatherContext(config);
|
|
|
|
this.loadingIndicator?.hide();
|
|
|
|
// Check if any notes were discovered
|
|
if (gatheredContext.notes.length === 0) {
|
|
new Notice('No notes discovered with the given configuration');
|
|
return;
|
|
}
|
|
|
|
// Step 2: Show checkbox selection modal
|
|
const selectionModal = new ContextSelectionModal(
|
|
this.app,
|
|
gatheredContext.notes,
|
|
async (selectedNotes: DiscoveredNote[]) => {
|
|
await this.showContextPreview(gatheredContext, selectedNotes, options);
|
|
}
|
|
);
|
|
selectionModal.open();
|
|
} catch (error) {
|
|
this.loadingIndicator?.hide();
|
|
console.error('Failed to gather context:', error);
|
|
new Notice('Failed to gather context: ' + error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show context preview with save/process options
|
|
* @param context Original gathered context
|
|
* @param selectedNotes User-selected notes
|
|
* @param options Command options
|
|
*/
|
|
private async showContextPreview(
|
|
context: GatheredContext,
|
|
selectedNotes: DiscoveredNote[],
|
|
options: CommandOptions
|
|
): Promise<void> {
|
|
try {
|
|
// Check if any notes selected
|
|
if (selectedNotes.length === 0) {
|
|
new Notice('No notes selected');
|
|
return;
|
|
}
|
|
|
|
this.loadingIndicator?.show('Aggregating content...');
|
|
|
|
// Re-aggregate with only selected notes (preserving backlink info)
|
|
const aggregated = await this.contextGatheringService.aggregateContent(
|
|
selectedNotes,
|
|
context.config.filterRecentSectionsOnly ? context.config.journalSectionDays : undefined
|
|
);
|
|
|
|
// Update context with selected notes
|
|
const finalContext: GatheredContext = {
|
|
...context,
|
|
notes: selectedNotes,
|
|
aggregatedContent: aggregated.content,
|
|
totalCharacters: aggregated.content.length,
|
|
totalNotes: selectedNotes.length
|
|
};
|
|
|
|
this.loadingIndicator?.hide();
|
|
|
|
// Determine button label based on command type
|
|
const processButtonLabel = options.commandType === 'save-raw'
|
|
? 'Save Context'
|
|
: 'Process with AI';
|
|
|
|
// Step 3: Show preview modal
|
|
const previewModal = new ContextPreviewModal(
|
|
this.app,
|
|
finalContext,
|
|
async () => await this.saveRawContext(finalContext),
|
|
async () => {
|
|
if (options.commandType === 'save-raw') {
|
|
await this.saveRawContext(finalContext);
|
|
} else {
|
|
await this.processContextWithAI(finalContext, options);
|
|
}
|
|
},
|
|
processButtonLabel
|
|
);
|
|
previewModal.open();
|
|
} catch (error) {
|
|
this.loadingIndicator?.hide();
|
|
console.error('Failed to preview context:', error);
|
|
new Notice('Failed to preview context: ' + error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save raw context to a note without AI processing
|
|
* @param context The gathered context to save
|
|
*/
|
|
private async saveRawContext(context: GatheredContext): Promise<void> {
|
|
try {
|
|
this.loadingIndicator?.show('Saving context...');
|
|
|
|
// Ensure OpenAugi folder exists
|
|
if (!await this.app.vault.adapter.exists('OpenAugi')) {
|
|
await this.app.vault.createFolder('OpenAugi');
|
|
}
|
|
|
|
// Generate filename
|
|
const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
const timeString = new Date().toISOString().split('T')[1].substring(0, 8).replace(/:/g, '-');
|
|
const filename = `Context ${timestamp} ${timeString}`;
|
|
const path = `OpenAugi/${filename}.md`;
|
|
|
|
// Build content
|
|
let content = `# Gathered Context\n\n`;
|
|
content += `**Source**: ${context.config.sourceMode}\n`;
|
|
content += `**Notes**: ${context.totalNotes}\n`;
|
|
content += `**Characters**: ${context.totalCharacters.toLocaleString()}\n`;
|
|
content += `**Timestamp**: ${context.timestamp}\n`;
|
|
|
|
if (context.config.sourceMode === 'linked-notes') {
|
|
content += `**Link Depth**: ${context.config.linkDepth}\n`;
|
|
}
|
|
|
|
content += `\n## Included Notes\n`;
|
|
content += context.notes.map(n => `- [[${n.file.basename}]]`).join('\n');
|
|
content += `\n\n---\n\n`;
|
|
content += context.aggregatedContent;
|
|
|
|
// Save file
|
|
await createFileWithCollisionHandling(this.app.vault, path, content);
|
|
|
|
this.loadingIndicator?.hide();
|
|
|
|
new Notice(`Context saved to: ${filename}`);
|
|
|
|
// Open the file
|
|
await this.openFileInNewTab(path);
|
|
} catch (error) {
|
|
this.loadingIndicator?.hide();
|
|
console.error('Failed to save raw context:', error);
|
|
new Notice('Failed to save context: ' + error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process context with AI (distill or publish)
|
|
* @param context The gathered context
|
|
* @param options Command options
|
|
*/
|
|
private async processContextWithAI(
|
|
context: GatheredContext,
|
|
options: CommandOptions
|
|
): Promise<void> {
|
|
// Show prompt selection modal with processing type option
|
|
const promptModal = new PromptSelectionModal(
|
|
this.app,
|
|
this.settings.promptsFolder,
|
|
async (promptConfig: PromptSelectionConfig) => {
|
|
await this.executeProcessing(context, promptConfig, options);
|
|
},
|
|
true, // Show processing type selector
|
|
'distill' // Default to distill
|
|
);
|
|
promptModal.open();
|
|
}
|
|
|
|
/**
|
|
* Execute AI processing with the selected prompt and processing type
|
|
* @param context The gathered context
|
|
* @param promptConfig Prompt selection configuration
|
|
* @param options Command options
|
|
*/
|
|
private async executeProcessing(
|
|
context: GatheredContext,
|
|
promptConfig: PromptSelectionConfig,
|
|
options: CommandOptions
|
|
): Promise<void> {
|
|
try {
|
|
// Check API key
|
|
if (!this.settings.apiKey) {
|
|
new Notice('Please set your OpenAI API key in the settings');
|
|
return;
|
|
}
|
|
|
|
// Read custom prompt if selected
|
|
let customPrompt: string | undefined;
|
|
if (promptConfig.useCustomPrompt && promptConfig.selectedPrompt) {
|
|
try {
|
|
customPrompt = await this.app.vault.read(promptConfig.selectedPrompt);
|
|
} catch (error) {
|
|
console.error('Failed to read custom prompt:', error);
|
|
new Notice('Failed to read custom prompt, using default');
|
|
}
|
|
}
|
|
|
|
const processingType = promptConfig.processingType || 'distill';
|
|
|
|
if (processingType === 'publish') {
|
|
await this.executePublish(context, customPrompt, promptConfig.selectedPrompt?.basename || 'default');
|
|
} else {
|
|
await this.executeDistill(context, customPrompt);
|
|
}
|
|
} catch (error) {
|
|
this.loadingIndicator?.hide();
|
|
console.error('Failed to process context:', error);
|
|
new Notice('Failed to process context: ' + error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute distill processing
|
|
* @param context The gathered context
|
|
* @param customPrompt Optional custom prompt
|
|
*/
|
|
private async executeDistill(
|
|
context: GatheredContext,
|
|
customPrompt?: string
|
|
): Promise<void> {
|
|
try {
|
|
this.loadingIndicator?.show('Distilling content...');
|
|
|
|
// Update services with latest API key and model
|
|
this.openAIService = new OpenAIService(this.settings.apiKey, this.getConfiguredModel());
|
|
this.distillService = new DistillService(
|
|
this.app,
|
|
this.openAIService,
|
|
this.settings
|
|
);
|
|
|
|
// Call distill API
|
|
const distilledData = await this.openAIService.distillContent(
|
|
context.aggregatedContent,
|
|
customPrompt
|
|
);
|
|
|
|
// Add source notes
|
|
distilledData.sourceNotes = context.notes.map(n => n.file.basename);
|
|
|
|
// Write files using first note as synthetic root
|
|
const syntheticRoot = context.notes[0].file;
|
|
const summaryPath = await this.fileService.writeDistilledFiles(syntheticRoot, distilledData);
|
|
|
|
this.loadingIndicator?.hide();
|
|
|
|
new Notice(`Successfully distilled! Created ${distilledData.notes.length} atomic notes`);
|
|
|
|
// Open the summary file
|
|
await this.openFileInNewTab(summaryPath);
|
|
} catch (error) {
|
|
this.loadingIndicator?.hide();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute publish processing
|
|
* @param context The gathered context
|
|
* @param customPrompt Optional custom prompt
|
|
* @param promptName Name of the prompt used
|
|
*/
|
|
private async executePublish(
|
|
context: GatheredContext,
|
|
customPrompt: string | undefined,
|
|
promptName: string
|
|
): Promise<void> {
|
|
try {
|
|
this.loadingIndicator?.show('Publishing content...');
|
|
|
|
// Update services with latest API key and model
|
|
this.openAIService = new OpenAIService(this.settings.apiKey, this.getConfiguredModel());
|
|
|
|
// Call publish API
|
|
const publishedContent = await this.openAIService.publishContent(
|
|
context.aggregatedContent,
|
|
customPrompt
|
|
);
|
|
|
|
// Write published post
|
|
const sourceNotes = context.notes.map(n => n.file.basename);
|
|
const publishedPath = await this.fileService.writePublishedPost(
|
|
publishedContent,
|
|
sourceNotes,
|
|
promptName
|
|
);
|
|
|
|
this.loadingIndicator?.hide();
|
|
|
|
new Notice('Successfully published blog post!');
|
|
|
|
// Open the published file
|
|
await this.openFileInNewTab(publishedPath);
|
|
} catch (error) {
|
|
this.loadingIndicator?.hide();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
} |