1.10.0 Update

This commit is contained in:
Tea 2026-05-12 16:28:45 -04:00
parent 4879d84188
commit e51eb40aaa
7 changed files with 501 additions and 348 deletions

View file

@ -1,4 +1,4 @@
# Multimuse Obsidian
# Multimuse Tracker
For use with the BRAT Plugin for the best experiance.
An Obsidian plugin for seamless integration with the MultiMuse Discord bot. Track Discord roleplay threads, send messages as muses directly from Obsidian, and automatically sync scene states.
@ -30,7 +30,7 @@ Install via [BRAT](https://obsidian.md/plugins?id=obsidian42-brat) with https://
### 2. Configure Plugin
1. Open Obsidian Settings → Multimuse Obsidian
1. Open Obsidian Settings → Multimuse Tracker
2. Paste your API key in the "API Key" field
3. Your Discord user ID will be automatically detected from the API key
4. Configure other settings:
@ -209,4 +209,4 @@ MIT
## Credits
- **Author**: BackstagePass Group
- **Plugin**: Multimuse Obsidian
- **Plugin**: Multimuse Tracker

View file

@ -1,6 +1,6 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import { builtinModules } from "module";
const isProduction = process.argv.includes("production");
@ -21,7 +21,7 @@ const ctx = await esbuild.context({
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
...builtinModules],
format: "cjs",
target: "es2018",
logLevel: "info",

438
main.ts
View file

@ -1,4 +1,4 @@
import { Plugin, PluginSettingTab, Setting, Notice, TFile, MetadataCache, App, requestUrl, Modal, Editor, MarkdownView } from 'obsidian';
import { Plugin, PluginSettingTab, Setting, Notice, TFile, TFolder, TAbstractFile, App, requestUrl, Modal, Editor, MarkdownView, CachedMetadata, RequestUrlResponse } from 'obsidian';
interface MultimuseObsidianSettings {
botApiUrl: string; // Bot HTTP API URL (hidden from user UI for security)
@ -40,6 +40,80 @@ interface MuseInfo {
muse_id?: string | null; // Optional: for API calls (alias-safe); display always uses name
}
type FrontmatterData = Record<string, unknown>;
type FrontmatterValue = string | number | boolean | string[];
interface AuthMeResponse {
user_id?: string | number;
}
interface MusesListResponse {
muses?: MuseInfo[];
}
interface SceneState {
replied?: boolean | string | null;
is_from_character?: boolean | string | null;
timestamp?: string | null;
your_last_post?: string | null;
posted_since_count?: number | null;
}
interface SceneQueryResponse {
tracked?: boolean;
state?: SceneState | null;
}
interface TrackedThread {
thread_id: string | number;
muse_name?: string;
muse_names?: string[];
participants?: number | string;
scene_path?: string;
scene_paths?: string[];
guild_id?: string | number | null;
thread_name?: string;
}
interface TrackedThreadsResponse {
threads?: TrackedThread[];
}
interface GuildMember {
id: string;
username: string;
display_name: string;
}
interface GuildMembersResponse {
members?: GuildMember[];
}
interface ApiErrorBody {
message?: string;
}
function parseJson<T>(text: string): T {
return JSON.parse(text) as T;
}
function getErrorMessage(error: unknown): string {
if (error && typeof error === 'object') {
const details = error as { message?: unknown; text?: unknown };
if (typeof details.message === 'string') return details.message;
if (typeof details.text === 'string') return details.text;
}
return 'Unknown error';
}
function getErrorStatus(error: unknown): number | undefined {
if (error && typeof error === 'object') {
const details = error as { status?: unknown };
if (typeof details.status === 'number') return details.status;
}
return undefined;
}
export default class MultimuseObsidian extends Plugin {
settings: MultimuseObsidianSettings;
pollIntervalId: number | null = null;
@ -71,7 +145,7 @@ export default class MultimuseObsidian extends Plugin {
id: 'check-discord-threads',
name: 'Check Discord Threads Now',
callback: () => {
this.checkAllThreads();
void this.checkAllThreads();
}
});
@ -81,7 +155,7 @@ export default class MultimuseObsidian extends Plugin {
name: 'Toggle Discord Polling',
callback: () => {
this.settings.enabled = !this.settings.enabled;
this.saveSettings();
void this.saveSettings();
if (this.settings.enabled) {
this.startPolling();
new Notice('Discord polling enabled');
@ -97,7 +171,7 @@ export default class MultimuseObsidian extends Plugin {
id: 'create-scene',
name: 'Create New Scene',
callback: () => {
this.createNewScene();
void this.createNewScene();
}
});
@ -106,7 +180,7 @@ export default class MultimuseObsidian extends Plugin {
id: 'sync-from-tracker',
name: 'Sync from Tracker',
callback: () => {
this.syncFromTracker();
void this.syncFromTracker();
}
});
@ -208,11 +282,11 @@ export default class MultimuseObsidian extends Plugin {
const intervalMs = this.settings.pollInterval * 60 * 1000;
this.pollIntervalId = window.setInterval(() => {
this.checkAllThreads();
void this.checkAllThreads();
}, intervalMs);
// Do an initial check
this.checkAllThreads();
void this.checkAllThreads();
}
stopPolling() {
@ -250,13 +324,47 @@ export default class MultimuseObsidian extends Plugin {
return headers;
}
async trackThread(params: {
threadId: string;
userId: string;
museName: string;
participants: number;
scenePath?: string;
guildId?: string | null;
characters?: string[];
}): Promise<RequestUrlResponse> {
const body: Record<string, unknown> = {
thread_id: params.threadId,
user_id: params.userId,
muse_name: params.museName,
participants: params.participants
};
if (params.scenePath) body.scene_path = params.scenePath;
if (params.guildId) body.guild_id = params.guildId;
if (params.characters && params.characters.length > 0) body.characters = params.characters;
return await requestUrl({
url: `${this.getBotApiUrl()}/api/v1/threads/track`,
method: 'POST',
headers: this.getApiHeaders(),
body: JSON.stringify(body)
});
}
getFrontmatter(cache: CachedMetadata | null): FrontmatterData | null {
if (!cache?.frontmatter) {
return null;
}
return cache.frontmatter as FrontmatterData;
}
/**
* Handle API response errors, especially authentication errors.
* @param response The response object from requestUrl
* @param context Context string for logging
* @returns true if error was handled, false otherwise
*/
handleApiError(response: any, context: string): boolean {
handleApiError(response: RequestUrlResponse, context: string): boolean {
if (response.status === 401) {
const errorMsg = 'API authentication failed. Please check your API key in settings.';
console.error(`[MultimuseObsidian] ${context}: ${errorMsg}`);
@ -291,7 +399,7 @@ export default class MultimuseObsidian extends Plugin {
});
if (response.status === 200) {
const data = JSON.parse(response.text);
const data = parseJson<AuthMeResponse>(response.text);
const userId = data.user_id;
if (userId) {
// Cache the user ID
@ -369,7 +477,7 @@ export default class MultimuseObsidian extends Plugin {
});
if (response.status === 200) {
const data = JSON.parse(response.text);
const data = parseJson<MusesListResponse>(response.text);
const muses: MuseInfo[] = data.muses || [];
// Cache all muses for each user ID (API already returns all accessible muses for all provided user IDs)
@ -400,7 +508,7 @@ export default class MultimuseObsidian extends Plugin {
}
async checkAllThreadsViaBotApi(): Promise<void> {
/**Check all scene files by querying the tracker API for linked scenes in batch.*/
/**Check all scene files against current thread_tracking state.*/
if (!this.settings.apiKey) {
return;
}
@ -416,34 +524,37 @@ export default class MultimuseObsidian extends Plugin {
return;
}
// Get all linked scenes from the API
const linkedUrl = `${this.getBotApiUrl()}/api/v1/scenes/linked?user_id=${primaryUserId}`;
const linkedResponse = await requestUrl({
url: linkedUrl,
// Get all Discord-side tracked threads from the current API.
const trackedUrl = `${this.getBotApiUrl()}/api/v1/threads/tracked?user_id=${primaryUserId}`;
const trackedResponse = await requestUrl({
url: trackedUrl,
method: 'GET',
headers: this.getApiHeaders()
});
if (linkedResponse.status !== 200) {
if (!this.handleApiError(linkedResponse, 'checkAllThreadsViaBotApi')) {
console.error(`[MultimuseObsidian] Failed to fetch linked scenes: ${linkedResponse.status} - ${linkedResponse.text}`);
if (trackedResponse.status !== 200) {
if (!this.handleApiError(trackedResponse, 'checkAllThreadsViaBotApi')) {
console.error(`[MultimuseObsidian] Failed to fetch tracked threads: ${trackedResponse.status} - ${trackedResponse.text}`);
}
return;
}
const linkedData = JSON.parse(linkedResponse.text);
const linkedThreads = linkedData.linked_threads || [];
const trackedData = parseJson<TrackedThreadsResponse>(trackedResponse.text);
const trackedThreads = trackedData.threads || [];
if (linkedThreads.length === 0) {
if (trackedThreads.length === 0) {
return;
}
// Create a map of scene_path -> thread info for quick lookup
const scenePathMap = new Map<string, any>();
for (const thread of linkedThreads) {
// Create a map of scene_path -> thread info for quick lookup.
const scenePathMap = new Map<string, TrackedThread>();
for (const thread of trackedThreads) {
if (thread.scene_path) {
scenePathMap.set(thread.scene_path, thread);
}
for (const scenePath of thread.scene_paths || []) {
scenePathMap.set(scenePath, thread);
}
}
// Get all scene files
@ -460,12 +571,13 @@ export default class MultimuseObsidian extends Plugin {
}
const cache = this.app.metadataCache.getFileCache(file);
if (!cache || !cache.frontmatter) {
const frontmatter = this.getFrontmatter(cache);
if (!frontmatter) {
continue;
}
// Skip if scene is marked as inactive
const isActive = cache.frontmatter['Is Active?'];
const isActive = frontmatter['Is Active?'];
if (isActive === false || isActive === 'false') {
continue;
}
@ -473,11 +585,11 @@ export default class MultimuseObsidian extends Plugin {
// Check if this scene is in the linked threads
const threadInfo = scenePathMap.get(file.path);
if (!threadInfo) {
// Scene not linked, try querying by thread_id and characters
const link = cache.frontmatter['Link'];
if (!link) continue;
// Scene not linked in thread_tracking, try querying by thread_id and characters.
const link = frontmatter['Link'];
if (typeof link !== 'string') continue;
const characters = this.getCharacterNames(cache.frontmatter);
const characters = this.getCharacterNames(frontmatter);
if (characters.length === 0) continue;
const threadId = this.extractThreadIdFromUrl(link);
@ -490,10 +602,10 @@ export default class MultimuseObsidian extends Plugin {
}
} else {
// Scene is linked - query its state
const characters = threadInfo.characters || [];
const characters = this.getCharacterNames(frontmatter);
if (characters.length === 0) continue;
const charactersParam = Array.isArray(characters) ? characters.join(',') : characters;
const charactersParam = characters.join(',');
// Use primary user ID for query
const primaryUserId = await this.getPrimaryUserId();
if (!primaryUserId) {
@ -508,7 +620,7 @@ export default class MultimuseObsidian extends Plugin {
});
if (queryResponse.status === 200) {
const queryData = JSON.parse(queryResponse.text);
const queryData = parseJson<SceneQueryResponse>(queryResponse.text);
if (queryData.tracked && queryData.state) {
const state = queryData.state;
@ -546,22 +658,23 @@ export default class MultimuseObsidian extends Plugin {
}
const cache = this.app.metadataCache.getFileCache(file);
if (!cache || !cache.frontmatter) {
const frontmatter = this.getFrontmatter(cache);
if (!cache || !frontmatter) {
return false;
}
// Skip if scene is marked as inactive
const isActive = cache.frontmatter['Is Active?'];
const isActive = frontmatter['Is Active?'];
if (isActive === false || isActive === 'false') {
return false; // Skip inactive scenes
}
const link = cache.frontmatter['Link'];
if (!link) {
const link = frontmatter['Link'];
if (typeof link !== 'string') {
return false;
}
const characters = this.getCharacterNames(cache.frontmatter);
const characters = this.getCharacterNames(frontmatter);
if (characters.length === 0) {
return false;
}
@ -594,7 +707,7 @@ export default class MultimuseObsidian extends Plugin {
return false;
}
const data = JSON.parse(response.text);
const data = parseJson<SceneQueryResponse>(response.text);
// If scene is not tracked, don't update anything
if (!data.tracked || !data.state) {
@ -621,9 +734,13 @@ export default class MultimuseObsidian extends Plugin {
}
}
async updateSceneFromState(file: TFile, cache: any, state: any): Promise<boolean> {
async updateSceneFromState(file: TFile, cache: CachedMetadata, state: SceneState): Promise<boolean> {
/**Update scene frontmatter from API state data.*/
let updated = false;
const frontmatter = this.getFrontmatter(cache);
if (!frontmatter) {
return false;
}
// Use replied or is_from_character (API may send either)
const repliedRaw = state.replied ?? state.is_from_character;
@ -641,7 +758,7 @@ export default class MultimuseObsidian extends Plugin {
}
// Update Replied? field - normalize boolean values for comparison
const currentRepliedRaw = cache.frontmatter['Replied?'];
const currentRepliedRaw = frontmatter['Replied?'];
// Handle both boolean and string values
const currentReplied = currentRepliedRaw === true || currentRepliedRaw === 'true' || currentRepliedRaw === 'True';
// true = you've replied (no need to reply), false = need to reply
@ -685,10 +802,10 @@ export default class MultimuseObsidian extends Plugin {
}
// Small delay to avoid checking during file creation
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise(resolve => window.setTimeout(resolve, 1000));
const cache = this.app.metadataCache.getFileCache(file);
if (cache?.frontmatter) {
if (this.getFrontmatter(cache)) {
// Sync Is Active? to the tracker so unchecking removes the scene from MultiMuse
await this.syncSceneActiveStatusToApi(file, cache);
// Sync Participants to the tracker so "your turn" / Replied? use the correct count
@ -721,7 +838,9 @@ export default class MultimuseObsidian extends Plugin {
const primaryUserId = await this.getPrimaryUserId();
if (!primaryUserId) return;
const threadId = this.extractThreadIdFromUrl(String(link));
if (typeof link !== 'string') return;
const threadId = this.extractThreadIdFromUrl(link);
const raw = cache.frontmatter?.['Is Active?'];
const isActive = raw !== false && raw !== 'false';
@ -776,9 +895,15 @@ export default class MultimuseObsidian extends Plugin {
const primaryUserId = await this.getPrimaryUserId();
if (!primaryUserId) return;
const threadId = this.extractThreadIdFromUrl(String(link));
if (typeof link !== 'string') return;
const threadId = this.extractThreadIdFromUrl(link);
const raw = cache.frontmatter?.['Participants'];
const participants = typeof raw === 'number' && raw >= 1 ? raw : parseInt(String(raw || '2'), 10);
const participants = typeof raw === 'number' && raw >= 1
? raw
: typeof raw === 'string'
? parseInt(raw, 10)
: 2;
if (isNaN(participants) || participants < 1) return;
try {
@ -827,10 +952,10 @@ export default class MultimuseObsidian extends Plugin {
return map;
}
collectMarkdownFiles(fileOrFolder: any, files: TFile[]): void {
collectMarkdownFiles(fileOrFolder: TAbstractFile, files: TFile[]): void {
if (fileOrFolder instanceof TFile && fileOrFolder.extension === 'md') {
files.push(fileOrFolder);
} else if ('children' in fileOrFolder) {
} else if (fileOrFolder instanceof TFolder) {
for (const child of fileOrFolder.children) {
this.collectMarkdownFiles(child, files);
}
@ -838,7 +963,7 @@ export default class MultimuseObsidian extends Plugin {
}
getCharacterNames(frontmatter: any): string[] {
getCharacterNames(frontmatter: FrontmatterData): string[] {
const characters = frontmatter['Characters'];
if (!characters) {
return [];
@ -879,7 +1004,7 @@ export default class MultimuseObsidian extends Plugin {
}
async updateFrontmatter(file: TFile, key: string, value: any): Promise<void> {
async updateFrontmatter(file: TFile, key: string, value: FrontmatterValue): Promise<void> {
const content = await this.app.vault.read(file);
// Parse frontmatter
@ -961,7 +1086,7 @@ export default class MultimuseObsidian extends Plugin {
});
if (response.status === 200) {
const data = JSON.parse(response.text);
const data = parseJson<MusesListResponse>(response.text);
muses = data.muses || [];
console.log(`[MultimuseObsidian] Found ${muses.length} muse(s) from ${userIds.length} user(s)`);
if (muses.length > 0) {
@ -996,7 +1121,7 @@ export default class MultimuseObsidian extends Plugin {
const selectedMuse = muses[selectedMuseIndex];
// Small delay to ensure previous modal is fully closed
await new Promise(resolve => setTimeout(resolve, 100));
await new Promise(resolve => window.setTimeout(resolve, 100));
// 3) Get Discord thread/channel link
const threadUrl = await this.showInputPrompt('Enter Discord thread/channel URL');
@ -1009,7 +1134,7 @@ export default class MultimuseObsidian extends Plugin {
}
// Small delay before opening location modal
await new Promise(resolve => setTimeout(resolve, 100));
await new Promise(resolve => window.setTimeout(resolve, 100));
// 4) Get location (RP folder) - pass muse name for context
console.log(`[MultimuseObsidian] createNewScene: About to select location for muse "${selectedMuse.name}"`);
@ -1027,7 +1152,7 @@ export default class MultimuseObsidian extends Plugin {
// 7) Create scene file
const filePath = `${location}/${sceneName}.md`;
const frontmatter: Record<string, any> = {
const frontmatter: Record<string, FrontmatterValue> = {
'Link': threadUrl,
'Characters': [selectedMuse.name],
'Participants': participants,
@ -1089,80 +1214,37 @@ export default class MultimuseObsidian extends Plugin {
// Mark this file as recently created to skip immediate checking
this.recentlyCreatedFiles.add(filePath);
// Remove from the set after 60 seconds (enough time for the scene to be registered and settled with the API)
setTimeout(() => {
window.setTimeout(() => {
this.recentlyCreatedFiles.delete(filePath);
console.log(`[MultimuseObsidian] Removed ${filePath} from recently created files - will now be checked by polling`);
}, 60000);
// 8) Register with bot API and optionally create thread tracking
// 8) Link the vault scene to the current Discord-side thread tracker.
try {
// Register scene
// Log the extracted thread info for debugging
console.debug(`Registering scene: threadId=${threadInfo.threadId}, guildId=${threadInfo.guildId}, channelId=${threadInfo.channelId}, url=${threadUrl}`);
console.debug(`Tracking scene: threadId=${threadInfo.threadId}, guildId=${threadInfo.guildId}, channelId=${threadInfo.channelId}, url=${threadUrl}`);
// Convert IDs to strings to avoid JavaScript number precision loss
// Discord IDs are larger than Number.MAX_SAFE_INTEGER, so we send them as strings
// Use primary user ID for scene registration
// Use primary user ID for thread tracking
const primaryUserId = await this.getPrimaryUserId();
if (!primaryUserId) {
new Notice('Failed to get user ID from API key. Please check your API key in settings.');
return;
}
const registerResponse = await requestUrl({
url: `${this.getBotApiUrl()}/api/v1/scenes/create`,
method: 'POST',
headers: this.getApiHeaders(),
body: JSON.stringify({
thread_id: threadInfo.threadId, // Send as string, let Python convert
user_id: primaryUserId, // Send as string
scene_path: createdFile.path,
characters: [selectedMuse.name],
participants: participants,
is_active: true,
guild_id: threadInfo.guildId || null // Send as string or null
})
const registerResponse = await this.trackThread({
threadId: threadInfo.threadId,
userId: primaryUserId,
museName: selectedMuse.name,
participants: participants,
scenePath: createdFile.path,
guildId: threadInfo.guildId || null,
characters: [selectedMuse.name]
});
console.debug(`Scene registration response: ${registerResponse.status} - ${registerResponse.text}`);
console.debug(`Thread tracking response: ${registerResponse.status} - ${registerResponse.text}`);
if (registerResponse.status === 200) {
// Scene registered successfully - proceed with optional operations
// Also create thread tracking (optional - don't fail if this errors)
// This may fail if bot can't access the thread, which is okay
try {
const trackBody: Record<string, unknown> = {
thread_id: threadInfo.threadId,
user_id: primaryUserId,
muse_name: selectedMuse.name,
participants: participants,
scene_path: createdFile.path,
guild_id: threadInfo.guildId || null
};
if (selectedMuse.muse_id) {
trackBody.muse_id = selectedMuse.muse_id;
}
const trackResponse = await requestUrl({
url: `${this.getBotApiUrl()}/api/v1/threads/track`,
method: 'POST',
headers: this.getApiHeaders(),
body: JSON.stringify(trackBody)
});
if (trackResponse.status === 200) {
console.debug('Thread tracking created successfully');
} else {
console.error(`Thread tracking failed: ${trackResponse.status} - ${trackResponse.text}`);
}
} catch (trackError: any) {
// Thread tracking is optional - scene is already registered
// Only log if it's not a 400 (which means bot can't access thread - expected)
if (trackError?.status !== 400) {
console.error('Thread tracking error (non-fatal):', trackError);
}
}
// Add to Base if configured
try {
if (this.settings.basePath) {
@ -1176,29 +1258,20 @@ export default class MultimuseObsidian extends Plugin {
await this.app.workspace.getLeaf(true).openFile(createdFile);
} else if (registerResponse.status === 401) {
// Authentication error - show helpful message
this.handleApiError(registerResponse, 'createNewScene - register scene');
new Notice('Scene created but failed to register with bot: Authentication failed. Check your API key.');
this.handleApiError(registerResponse, 'createNewScene - track thread');
new Notice('Scene created but failed to track with bot: Authentication failed. Check your API key.');
} else {
const errorText = registerResponse.text || 'Unknown error';
console.error(`Failed to register scene: ${registerResponse.status} - ${errorText}`);
new Notice(`Scene created but failed to register with bot: ${registerResponse.status}`);
console.error(`Failed to track thread: ${registerResponse.status} - ${errorText}`);
new Notice(`Scene created but failed to track with bot: ${registerResponse.status}`);
}
} catch (error: any) {
} catch (error) {
// Log the full error for debugging
console.error('Error registering scene:', error);
const errorMessage = error?.message || error?.text || 'Unknown error';
console.error('Error tracking thread:', error);
const errorMessage = getErrorMessage(error);
console.error('Error details:', errorMessage);
// Only show error if scene registration itself failed
// Thread tracking errors are handled above and shouldn't reach here
if (error?.status === 400 && error?.message?.includes('threads/track')) {
// This is a thread tracking error that slipped through - ignore it
// Scene was already registered successfully
new Notice(`Scene created: ${sceneName}`);
await this.app.workspace.getLeaf(true).openFile(createdFile);
} else {
new Notice(`Scene created but failed to register with bot: ${errorMessage}`);
}
new Notice(`Scene created but failed to track with bot: ${errorMessage}`);
}
}
@ -1232,7 +1305,7 @@ export default class MultimuseObsidian extends Plugin {
return;
}
const data = JSON.parse(response.text);
const data = parseJson<TrackedThreadsResponse>(response.text);
const threads = data.threads || [];
if (threads.length === 0) {
@ -1248,7 +1321,7 @@ export default class MultimuseObsidian extends Plugin {
for (const thread of threads) {
const threadId = String(thread.thread_id ?? '');
const museName = thread.muse_name;
const museName = thread.muse_name || 'Muse';
const participants = thread.participants || 2;
const existingScenePath = thread.scene_path;
@ -1298,7 +1371,7 @@ export default class MultimuseObsidian extends Plugin {
const sceneName = thread.thread_name || `${museName} - Thread ${threadId}`;
const filePath = `${location}/${sceneName}.md`;
const frontmatter: Record<string, any> = {
const frontmatter: Record<string, FrontmatterValue> = {
'Link': threadUrl,
'Characters': [museName],
'Participants': participants,
@ -1360,30 +1433,25 @@ export default class MultimuseObsidian extends Plugin {
// Mark this file as recently created to skip immediate checking
this.recentlyCreatedFiles.add(filePath);
// Remove from the set after 30 seconds (enough time for the scene to be registered with the API)
setTimeout(() => {
window.setTimeout(() => {
this.recentlyCreatedFiles.delete(filePath);
}, 30000);
// Register with bot
// Use primary user ID for scene registration
// Link the created note back to the current Discord-side thread tracker.
const primaryUserId = await this.getPrimaryUserId();
if (!primaryUserId) {
console.error('Failed to get user ID from API key, skipping scene registration');
console.error('Failed to get user ID from API key, skipping thread tracking update');
continue;
}
await requestUrl({
url: `${this.getBotApiUrl()}/api/v1/scenes/create`,
method: 'POST',
headers: this.getApiHeaders(),
body: JSON.stringify({
thread_id: threadId, // Send as string to avoid precision loss
user_id: primaryUserId, // Send as string
scene_path: filePath,
characters: [museName],
participants: participants,
guild_id: guildId || null, // Include guild_id if available
})
await this.trackThread({
threadId: threadId,
userId: primaryUserId,
museName: museName,
participants: Number(participants) || 2,
scenePath: filePath,
guildId: guildId ? String(guildId) : null,
characters: [museName]
});
// Add to Base if configured
@ -1403,7 +1471,7 @@ export default class MultimuseObsidian extends Plugin {
// ========= BASE INTEGRATION =========
async addSceneToBase(file: TFile, frontmatter: any): Promise<void> {
async addSceneToBase(file: TFile, frontmatter: FrontmatterData): Promise<void> {
/**Add scene to Obsidian Base with characters as variables.*/
if (!this.settings.basePath) return;
@ -1457,7 +1525,7 @@ export default class MultimuseObsidian extends Plugin {
}
}
async updateBaseRecord(file: TFile, thread: any): Promise<void> {
async updateBaseRecord(file: TFile, thread: TrackedThread): Promise<void> {
/**Update existing Base record for a scene.*/
if (!this.settings.basePath) return;
@ -1482,7 +1550,7 @@ export default class MultimuseObsidian extends Plugin {
const lines = baseContent.split('\n');
const updatedLines = lines.map(line => {
if (line.includes(`| ${sceneName} |`)) {
const characters = [thread.muse_name];
const characters = [thread.muse_name || 'Muse'];
const link = thread.guild_id && thread.guild_id !== '0'
? `https://discord.com/channels/${thread.guild_id}/${thread.thread_id}`
: `https://discord.com/channels/0/${thread.thread_id}`;
@ -1597,19 +1665,17 @@ export default class MultimuseObsidian extends Plugin {
return `${RP_ROOT}/${relPath}`;
}
showSuggester(items: string[], values: any[], title?: string): Promise<number | null> {
showSuggester<T>(items: string[], _values: T[], title?: string): Promise<number | null> {
return new Promise((resolve) => {
// Create a modal with buttons
const modal = new (class extends Modal {
selectedIndex: number | null = null;
items: string[];
values: any[];
titleText: string;
constructor(app: App, items: string[], values: any[], titleText?: string) {
constructor(app: App, items: string[], titleText?: string) {
super(app);
this.items = items;
this.values = values;
this.titleText = titleText || 'Select an option';
// Set title in constructor to ensure it's set before modal opens
this.titleEl.textContent = this.titleText;
@ -1629,26 +1695,23 @@ export default class MultimuseObsidian extends Plugin {
if (match) {
const contextInfo = match[1];
const infoEl = contentEl.createEl('div', {
attr: {
style: 'margin-bottom: 20px; padding: 10px; background: var(--background-modifier-border); border-radius: 4px;'
}
cls: 'multimuse-scene-context'
});
infoEl.createEl('strong', {
text: `Creating scene file for: ${contextInfo}`,
attr: { style: 'color: var(--text-normal); font-size: 1.1em;' }
cls: 'multimuse-scene-context-title'
});
}
const descEl = contentEl.createEl('p', {
contentEl.createEl('p', {
text: 'Choose where to create the scene file:',
attr: { style: 'margin-top: 10px; margin-bottom: 15px; color: var(--text-muted);' }
cls: 'multimuse-scene-location-desc'
});
}
this.items.forEach((item, index) => {
const button = contentEl.createEl('button', {
text: item,
cls: 'mod-cta',
attr: { style: 'width: 100%; margin: 5px 0;' }
cls: ['mod-cta', 'multimuse-suggester-button']
});
button.onclick = () => {
this.selectedIndex = index;
@ -1660,11 +1723,11 @@ export default class MultimuseObsidian extends Plugin {
onClose() {
resolve(this.selectedIndex);
}
})(this.app, items, values, title);
})(this.app, items, title);
console.log(`[MultimuseObsidian] showSuggester: Opening modal with ${items.length} items, title: ${title || 'Select an option'}`);
// Use requestAnimationFrame to ensure modal opens after any previous modals are fully closed
requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
modal.open();
});
});
@ -1682,9 +1745,9 @@ export default class MultimuseObsidian extends Plugin {
this.titleEl.textContent = prompt;
this.inputEl = this.contentEl.createEl('input', {
type: 'text',
value: defaultValue || ''
value: defaultValue || '',
cls: 'multimuse-input'
});
this.inputEl.style.width = '100%';
this.inputEl.onkeydown = (e) => {
if (e.key === 'Enter') {
this.value = this.inputEl.value;
@ -1722,8 +1785,9 @@ export default class MultimuseObsidian extends Plugin {
new Notice('No frontmatter. Add a Link property (Discord thread URL) to this note.');
return;
}
const link = cache.frontmatter['Link'];
if (!link) {
const frontmatter = this.getFrontmatter(cache);
const link = frontmatter?.['Link'];
if (typeof link !== 'string') {
new Notice('No Link property. Add the Discord thread URL to frontmatter to use @ mentions.');
return;
}
@ -1736,7 +1800,7 @@ export default class MultimuseObsidian extends Plugin {
new Notice('API key required in plugin settings to fetch server members.');
return;
}
let members: { id: string; username: string; display_name: string }[];
let members: GuildMember[];
try {
const url = `${this.getBotApiUrl()}/api/v1/guilds/${threadInfo.guildId}/members`;
const response = await requestUrl({
@ -1750,7 +1814,7 @@ export default class MultimuseObsidian extends Plugin {
}
return;
}
const data = JSON.parse(response.text);
const data = parseJson<GuildMembersResponse>(response.text);
members = data.members || [];
} catch (e) {
console.error('[MultimuseObsidian] insertMention fetch error:', e);
@ -1797,19 +1861,20 @@ export default class MultimuseObsidian extends Plugin {
// Get frontmatter
const cache = this.app.metadataCache.getFileCache(file);
if (!cache || !cache.frontmatter) {
const frontmatter = this.getFrontmatter(cache);
if (!frontmatter) {
new Notice('File does not have frontmatter. Please add Link and Characters properties.');
return;
}
// Extract link and characters
const link = cache.frontmatter['Link'];
if (!link) {
const link = frontmatter?.['Link'];
if (typeof link !== 'string') {
new Notice('No Link property found in frontmatter. Please add a Discord thread URL.');
return;
}
const characters = this.getCharacterNames(cache.frontmatter);
const characters = this.getCharacterNames(frontmatter);
if (characters.length === 0) {
new Notice('No Characters property found in frontmatter. Please add at least one character name.');
return;
@ -1861,7 +1926,7 @@ export default class MultimuseObsidian extends Plugin {
});
if (response.status === 200) {
const data = JSON.parse(response.text);
const data = parseJson<MusesListResponse>(response.text);
muses = data.muses || [];
} else {
if (!this.handleApiError(response, 'sendSelectionAsMuse - fetch muses')) {
@ -1909,17 +1974,16 @@ export default class MultimuseObsidian extends Plugin {
});
if (response.status === 200) {
const data = JSON.parse(response.text);
new Notice(`Message sent as ${selectedMuse}!`);
} else {
if (!this.handleApiError(response, 'sendSelectionAsMuse - post message')) {
const errorData = JSON.parse(response.text);
const errorData = parseJson<ApiErrorBody>(response.text);
new Notice(`Failed to send message: ${errorData.message || response.status}`);
}
}
} catch (error: any) {
} catch (error) {
console.error('Error sending message:', error);
const errorMessage = error?.message || error?.text || 'Unknown error';
const errorMessage = getErrorMessage(error);
new Notice(`Failed to send message: ${errorMessage}`);
}
}
@ -1938,7 +2002,9 @@ class MultimuseObsidianSettingTab extends PluginSettingTab {
containerEl.empty();
containerEl.createEl('h2', { text: 'Multimuse Obsidian Settings' });
new Setting(containerEl)
.setName('Multimuse Settings')
.setHeading();
// Enable/Disable toggle
new Setting(containerEl)
@ -1997,7 +2063,9 @@ class MultimuseObsidianSettingTab extends PluginSettingTab {
}));
// Scene Properties Tracking
containerEl.createEl('h3', { text: 'Scene Properties' });
new Setting(containerEl)
.setName('Scene Properties')
.setHeading();
new Setting(containerEl)
.setName('Track Roleplay Property')
@ -2059,8 +2127,8 @@ class MultimuseObsidianSettingTab extends PluginSettingTab {
.setName('Detected User ID')
.setDesc(`Your Discord user ID (automatically detected from API key): ${this.plugin.settings.cachedUserId}`)
.addText(text => {
text.setValue(this.plugin.settings.cachedUserId)
.setDisabled(true);
text.setValue(this.plugin.settings.cachedUserId);
text.inputEl.disabled = true;
});
}
@ -2084,14 +2152,16 @@ class MultimuseObsidianSettingTab extends PluginSettingTab {
.setButtonText('Check Now')
.setCta()
.onClick(() => {
this.plugin.checkAllThreads();
void this.plugin.checkAllThreads();
new Notice('Checking Discord threads...');
}));
// Info section
containerEl.createEl('hr');
const infoEl = containerEl.createEl('div');
infoEl.createEl('h3', { text: 'How It Works' });
new Setting(infoEl)
.setName('How It Works')
.setHeading();
infoEl.createEl('p', { text: 'This plugin queries the Multimuse API to check if your scene files match tracked threads and updates the "Replied?" and "Participants" fields.' });
infoEl.createEl('p', { text: '• Scenes are matched by Link (thread_id) and Characters properties' });
infoEl.createEl('p', { text: '• If scene matches a tracked thread: Updates Replied? only (true = you replied, false = need to reply). Participants is always editable in frontmatter.' });

View file

@ -1,9 +1,9 @@
{
"id": "multimuse-obsidian",
"name": "Multimuse Obsidian",
"version": "1.6.1",
"minAppVersion": "1.0.0",
"description": "Roleplay sync tool for use with the MultiMuse bot in discord",
"id": "multimuse-tracker",
"name": "Multimuse Tracker",
"version": "1.10.0",
"minAppVersion": "1.4.0",
"description": "Roleplay sync tool for use with the MultiMuse bot in discord.",
"author": "BackstagePass Group",
"authorUrl": "",
"fundingUrl": "",

354
package-lock.json generated
View file

@ -1,19 +1,18 @@
{
"name": "discord-scene-tracker",
"version": "1.0.0",
"name": "multimuse-tracker",
"version": "1.10.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "discord-scene-tracker",
"version": "1.0.0",
"name": "multimuse-tracker",
"version": "1.10.0",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"esbuild": "^0.28.0",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
@ -44,10 +43,27 @@
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.3.tgz",
"integrity": "sha512-1Mlz934GvbgdDmt26rTLmf03cAgLg5HyOgJN+ZGCeP3Q9ynYTNMn2/LQxIl7Uy+o4K6Rfi2OuLsr12JQQR8gNg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
@ -58,13 +74,13 @@
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.3.tgz",
"integrity": "sha512-XvJsYo3dO3Pi4kpalkyMvfQsjxPWHYjoX4MDiB/FUM4YMfWcXa5l4VCwFWVYI1+92yxqjuqrhNg0CZg3gSouyQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
@ -75,13 +91,13 @@
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.3.tgz",
"integrity": "sha512-nuV2CmLS07Gqh5/GrZLuqkU9Bm6H6vcCspM+zjp9TdQlxJtIe+qqEXQChmfc7nWdyr/yz3h45Utk1tUn8Cz5+A==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
@ -92,13 +108,13 @@
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.3.tgz",
"integrity": "sha512-01Hxaaat6m0Xp9AXGM8mjFtqqwDjzlMP0eQq9zll9U85ttVALGCGDuEvra5Feu/NbP5AEP1MaopPwzsTcUq1cw==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
@ -109,13 +125,13 @@
"darwin"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.3.tgz",
"integrity": "sha512-Eo2gq0Q/er2muf8Z83X21UFoB7EU6/m3GNKvrhACJkjVThd0uA+8RfKpfNhuMCl1bKRfBzKOk6xaYKQZ4lZqvA==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
@ -126,13 +142,13 @@
"darwin"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.3.tgz",
"integrity": "sha512-CN62ESxaquP61n1ZjQP/jZte8CE09M6kNn3baos2SeUfdVBkWN5n6vGp2iKyb/bm/x4JQzEvJgRHLGd5F5b81w==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
@ -143,13 +159,13 @@
"freebsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.3.tgz",
"integrity": "sha512-feq+K8TxIznZE+zhdVurF3WNJ/Sa35dQNYbaqM/wsCbWdzXr5lyq+AaTUSER2cUR+SXPnd/EY75EPRjf4s1SLg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
@ -160,13 +176,13 @@
"freebsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.3.tgz",
"integrity": "sha512-CLP3EgyNuPcg2cshbwkqYy5bbAgK+VhyfMU7oIYyn+x4Y67xb5C5ylxsNUjRmr8BX+MW3YhVNm6Lq6FKtRTWHQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
@ -177,13 +193,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.3.tgz",
"integrity": "sha512-JHeZXD4auLYBnrKn6JYJ0o5nWJI9PhChA/Nt0G4MvLaMrvXuWnY93R3a7PiXeJQphpL1nYsaMcoV2QtuvRnF/g==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
@ -194,13 +210,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.3.tgz",
"integrity": "sha512-FyXlD2ZjZqTFh0sOQxFDiWG1uQUEOLbEh9gKN/7pFxck5Vw0qjWSDqbn6C10GAa1rXJpwsntHcmLqydY9ST9ZA==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
@ -211,13 +227,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.3.tgz",
"integrity": "sha512-OrDGMvDBI2g7s04J8dh8/I7eSO+/E7nMDT2Z5IruBfUO/RiigF1OF6xoH33Dn4W/OwAWSUf1s2nXamb28ZklTA==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
@ -228,13 +244,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.3.tgz",
"integrity": "sha512-DcnUpXnVCJvmv0TzuLwKBC2nsQHle8EIiAJiJ+PipEVC16wHXaPEKP0EqN8WnBe0TPvMITOUlP2aiL5YMld+CQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
@ -245,13 +261,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.3.tgz",
"integrity": "sha512-BDYf/l1WVhWE+FHAW3FzZPtVlk9QsrwsxGzABmN4g8bTjmhazsId3h127pliDRRu5674k1Y2RWejbpN46N9ZhQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
@ -262,13 +278,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.3.tgz",
"integrity": "sha512-WViAxWYMRIi+prTJTyV1wnqd2mS2cPqJlN85oscVhXdb/ZTFJdrpaqm/uDsZPGKHtbg5TuRX/ymKdOSk41YZow==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
@ -279,13 +295,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.3.tgz",
"integrity": "sha512-Iw8lkNHUC4oGP1O/KhumcVy77u2s6+KUjieUqzEU3XuWJqZ+AY7uVMrrCbAiwWTkpQHkr00BuXH5RpC6Sb/7Ug==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
@ -296,13 +312,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.3.tgz",
"integrity": "sha512-0AGkWQMzeoeAtXQRNB3s4J1/T2XbigM2/Mn2yU1tQSmQRmHIZdkGbVq2A3aDdNslPyhb9/lH0S5GMTZ4xsjBqg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
@ -313,13 +329,30 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.3.tgz",
"integrity": "sha512-4+rR/WHOxIVh53UIQIICryjdoKdHsFZFD4zLSonJ9RRw7bhKzVyXbnRPsWSfwybYqw9sB7ots/SYyufL1mBpEg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
@ -330,13 +363,30 @@
"netbsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.3.tgz",
"integrity": "sha512-cVpWnkx9IYg99EjGxa5Gc0XmqumtAwK3aoz7O4Dii2vko+qXbkHoujWA68cqXjhh6TsLaQelfDO4MVnyr+ODeA==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
@ -347,13 +397,30 @@
"openbsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.3.tgz",
"integrity": "sha512-RxmhKLbTCDAY2xOfrww6ieIZkZF+KBqG7S2Ako2SljKXRFi+0863PspK74QQ7JpmWwncChY25JTJSbVBYGQk2Q==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
@ -364,13 +431,13 @@
"sunos"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.3.tgz",
"integrity": "sha512-0r36VeEJ4efwmofxVJRXDjVRP2jTmv877zc+i+Pc7MNsIr38NfsjkQj23AfF7l0WbB+RQ7VUb+LDiqC/KY/M/A==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
@ -381,13 +448,13 @@
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.3.tgz",
"integrity": "sha512-wgO6rc7uGStH22nur4aLFcq7Wh86bE9cOFmfTr/yxN3BXvDEdCSXyKkO+U5JIt53eTOgC47v9k/C1bITWL/Teg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
@ -398,13 +465,13 @@
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.3.tgz",
"integrity": "sha512-FdVl64OIuiKjgXBjwZaJLKp0eaEckifbhn10dXWhysMJkWblg3OEEGKSIyhiD5RSgAya8WzP3DNkngtIg3Nt7g==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
@ -415,7 +482,7 @@
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
@ -830,9 +897,9 @@
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -897,9 +964,9 @@
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -920,19 +987,6 @@
"node": ">=8"
}
},
"node_modules/builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -1061,9 +1115,9 @@
}
},
"node_modules/esbuild": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.3.tgz",
"integrity": "sha512-9n3AsBRe6sIyOc6kmoXg2ypCLgf3eZSraWFRpnkto+svt8cZNuKTkb1bhQcitBcvIqjNiK7K0J3KPmwGSfkA8g==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@ -1071,31 +1125,35 @@
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.17.3",
"@esbuild/android-arm64": "0.17.3",
"@esbuild/android-x64": "0.17.3",
"@esbuild/darwin-arm64": "0.17.3",
"@esbuild/darwin-x64": "0.17.3",
"@esbuild/freebsd-arm64": "0.17.3",
"@esbuild/freebsd-x64": "0.17.3",
"@esbuild/linux-arm": "0.17.3",
"@esbuild/linux-arm64": "0.17.3",
"@esbuild/linux-ia32": "0.17.3",
"@esbuild/linux-loong64": "0.17.3",
"@esbuild/linux-mips64el": "0.17.3",
"@esbuild/linux-ppc64": "0.17.3",
"@esbuild/linux-riscv64": "0.17.3",
"@esbuild/linux-s390x": "0.17.3",
"@esbuild/linux-x64": "0.17.3",
"@esbuild/netbsd-x64": "0.17.3",
"@esbuild/openbsd-x64": "0.17.3",
"@esbuild/sunos-x64": "0.17.3",
"@esbuild/win32-arm64": "0.17.3",
"@esbuild/win32-ia32": "0.17.3",
"@esbuild/win32-x64": "0.17.3"
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
},
"node_modules/escape-string-regexp": {
@ -1456,9 +1514,9 @@
}
},
"node_modules/flatted": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
@ -1777,9 +1835,9 @@
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@ -1942,9 +2000,9 @@
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {

View file

@ -1,7 +1,7 @@
{
"name": "multimuse-obsidian",
"version": "1.3.0",
"description": "Obsidian plugin for MultiMuse bot integration - track Discord threads and send messages as muses",
"name": "multimuse-tracker",
"version": "1.10.0",
"description": "MultiMuse bot integration for tracking Discord threads and sending messages as muses",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
@ -15,11 +15,9 @@
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"esbuild": "^0.28.0",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
}

View file

@ -17,3 +17,30 @@
border-top: none;
}
.multimuse-scene-context {
margin-bottom: 20px;
padding: 10px;
background: var(--background-modifier-border);
border-radius: 4px;
}
.multimuse-scene-context-title {
color: var(--text-normal);
font-size: 1.1em;
}
.multimuse-scene-location-desc {
margin-top: 10px;
margin-bottom: 15px;
color: var(--text-muted);
}
.multimuse-suggester-button,
.multimuse-input {
width: 100%;
}
.multimuse-suggester-button {
margin: 5px 0;
}