ksawl_obsidian-alchemist/lint_results.json
Serhii Karavashkin 294c57015f fix(alchemist): full compliance remediation for Obsidian Community Plugin submission
- Refactored ISystemAdapter & ElectronSystemAdapter with strict types
- Defined IAlchemistPlugin interface for type-safe saveSettings() access
- Fixed AudioModule: async IIFE, Buffer→ArrayBuffer casts, spawn typing
- Fixed DataviewModule: ObsidianAppWithPlugins interface, floating promises
- Fixed TextBundleModule: Uint8Array for fs.writeFileSync, ArrayBuffer casts
- Fixed settings.ts: removed normalizePath, prefer-create-el, sentence-case
- Fixed logic.ts: underscore-prefixed unused regex match args
- Fixed SmartPasteModule: removed unused imports
- Fixed paste-cleaner/logic.ts: underscore-prefixed caught error
- Updated manifest.json minAppVersion to 1.6.6 for FileManager.trashFile
- Added missing settings fields: recursionDepth, includeImages/Audio/Video/PDF
- Build: tsc + esbuild pass cleanly, dist/ updated
2026-05-03 23:11:34 +03:00

1 line
86 KiB
JSON

[{"filePath":"/web/obsidian/new/src/plugins/alchemist/src/core/IAlchemistModule.ts","messages":[{"ruleId":"no-undef","severity":2,"message":"'Electron' is not defined.","line":23,"column":29,"nodeType":"Identifier","messageId":"undef","endLine":23,"endColumn":37},{"ruleId":"no-undef","severity":2,"message":"'Electron' is not defined.","line":23,"column":66,"nodeType":"Identifier","messageId":"undef","endLine":23,"endColumn":74},{"ruleId":"no-undef","severity":2,"message":"'Electron' is not defined.","line":24,"column":29,"nodeType":"Identifier","messageId":"undef","endLine":24,"endColumn":37},{"ruleId":"no-undef","severity":2,"message":"'Electron' is not defined.","line":24,"column":66,"nodeType":"Identifier","messageId":"undef","endLine":24,"endColumn":74}],"suppressedMessages":[],"errorCount":4,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { App, Plugin } from 'obsidian';\nimport { AlchemistSettings } from '../settings';\n\nexport interface AlchemistContext {\n app: App;\n settings: AlchemistSettings;\n system: ISystemAdapter;\n plugin: Plugin;\n}\n\nexport interface IAlchemistModule {\n id: string;\n load(context: AlchemistContext): Promise<void>;\n unload(): Promise<void>;\n onSettingsChange(newSettings: AlchemistSettings): Promise<void>;\n}\n\nexport interface ISystemAdapter {\n fs: typeof import('fs');\n path: typeof import('path');\n os: typeof import('os');\n spawn: typeof import('child_process').spawn;\n showSaveDialog(options: Electron.SaveDialogOptions): Promise<Electron.SaveDialogReturnValue>;\n showOpenDialog(options: Electron.OpenDialogOptions): Promise<Electron.OpenDialogReturnValue>;\n}\n","usedDeprecatedRules":[]},{"filePath":"/web/obsidian/new/src/plugins/alchemist/src/core/SystemAdapter.ts","messages":[{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":8,"column":21,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":8,"endColumn":24,"fix":{"range":[305,308],"text":"unknown"},"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[305,308],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[305,308],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":11,"column":15,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":11,"endColumn":61},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of an `any` typed value.","line":11,"column":26,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":11,"endColumn":49},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":11,"column":37,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":11,"endColumn":40,"fix":{"range":[367,370],"text":"unknown"},"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[367,370],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[367,370],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .require on an `any` value.","line":11,"column":42,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":11,"endColumn":49},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":12,"column":21,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":12,"endColumn":24,"fix":{"range":[413,416],"text":"unknown"},"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[413,416],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[413,416],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":14,"column":13,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":14,"endColumn":65},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of an `any` typed value.","line":14,"column":22,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":14,"endColumn":45},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":14,"column":33,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":14,"endColumn":36,"fix":{"range":[464,467],"text":"unknown"},"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[464,467],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[464,467],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .require on an `any` value.","line":14,"column":38,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":14,"endColumn":45},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'e' is defined but never used. Allowed unused caught errors must match /^_/u.","line":15,"column":18,"nodeType":"Identifier","messageId":"unusedVar","endLine":15,"endColumn":19},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":16,"column":13,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":16,"endColumn":37},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .remote on an `any` value.","line":16,"column":31,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":16,"endColumn":37},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":19,"column":9,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":19,"endColumn":63},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .dialog on an `any` value.","line":19,"column":39,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":19,"endColumn":45},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .dialog on an `any` value.","line":19,"column":57,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":19,"endColumn":63},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":20,"column":9,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":20,"endColumn":48},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of an `any` typed value.","line":20,"column":19,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":20,"endColumn":42},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":20,"column":30,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":20,"endColumn":33,"fix":{"range":[662,665],"text":"unknown"},"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[662,665],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[662,665],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .require on an `any` value.","line":20,"column":35,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":20,"endColumn":42},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":21,"column":9,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":21,"endColumn":52},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of an `any` typed value.","line":21,"column":21,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":21,"endColumn":44},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":21,"column":32,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":21,"endColumn":35,"fix":{"range":[713,716],"text":"unknown"},"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[713,716],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[713,716],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .require on an `any` value.","line":21,"column":37,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":21,"endColumn":44},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":22,"column":9,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":22,"endColumn":48},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of an `any` typed value.","line":22,"column":19,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":22,"endColumn":42},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":22,"column":30,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":22,"endColumn":33,"fix":{"range":[764,767],"text":"unknown"},"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[764,767],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[764,767],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .require on an `any` value.","line":22,"column":35,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":22,"endColumn":42},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":23,"column":9,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":23,"endColumn":68},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of an `any` typed value.","line":23,"column":22,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":23,"endColumn":45},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":23,"column":33,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":23,"endColumn":36,"fix":{"range":[816,819],"text":"unknown"},"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[816,819],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[816,819],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .require on an `any` value.","line":23,"column":38,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":23,"endColumn":45},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .spawn on an `any` value.","line":23,"column":63,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":23,"endColumn":68},{"ruleId":"no-undef","severity":2,"message":"'Electron' is not defined.","line":26,"column":35,"nodeType":"Identifier","messageId":"undef","endLine":26,"endColumn":43},{"ruleId":"no-undef","severity":2,"message":"'Electron' is not defined.","line":26,"column":72,"nodeType":"Identifier","messageId":"undef","endLine":26,"endColumn":80},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of an `any` typed value.","line":27,"column":22,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":27,"endColumn":48},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .showSaveDialog on an `any` value.","line":27,"column":34,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":27,"endColumn":48},{"ruleId":"no-undef","severity":2,"message":"'Electron' is not defined.","line":30,"column":35,"nodeType":"Identifier","messageId":"undef","endLine":30,"endColumn":43},{"ruleId":"no-undef","severity":2,"message":"'Electron' is not defined.","line":30,"column":72,"nodeType":"Identifier","messageId":"undef","endLine":30,"endColumn":80},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of an `any` typed value.","line":31,"column":22,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":31,"endColumn":48},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .showOpenDialog on an `any` value.","line":31,"column":34,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":31,"endColumn":48}],"suppressedMessages":[],"errorCount":40,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":8,"fixableWarningCount":0,"source":"import { ISystemAdapter } from './IAlchemistModule';\n\nexport class ElectronSystemAdapter implements ISystemAdapter {\n public fs: typeof import('fs');\n public path: typeof import('path');\n public os: typeof import('os');\n public spawn: typeof import('child_process').spawn;\n private dialog: any;\n\n constructor() {\n const electron = (window as any).require('electron');\n let remote: any;\n try {\n remote = (window as any).require('@electron/remote');\n } catch (e) {\n remote = electron.remote;\n }\n\n this.dialog = remote ? remote.dialog : electron.dialog;\n this.fs = (window as any).require('fs');\n this.path = (window as any).require('path');\n this.os = (window as any).require('os');\n this.spawn = (window as any).require('child_process').spawn;\n }\n\n async showSaveDialog(options: Electron.SaveDialogOptions): Promise<Electron.SaveDialogReturnValue> {\n return await this.dialog.showSaveDialog(options);\n }\n\n async showOpenDialog(options: Electron.OpenDialogOptions): Promise<Electron.OpenDialogReturnValue> {\n return await this.dialog.showOpenDialog(options);\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/web/obsidian/new/src/plugins/alchemist/src/features/audio-converter/AudioModule.ts","messages":[{"ruleId":"@typescript-eslint/no-misused-promises","severity":2,"message":"Promise returned in function argument where a void return was expected.","line":178,"column":28,"nodeType":"ArrowFunctionExpression","messageId":"voidReturnArgument","endLine":259,"endColumn":10},{"ruleId":"no-async-promise-executor","severity":2,"message":"Promise executor functions should not be async.","line":178,"column":28,"nodeType":"Identifier","messageId":"async","endLine":178,"endColumn":33},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of an `any` typed value.","line":193,"column":60,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":193,"endColumn":73},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .toString on an `any` value.","line":193,"column":65,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":193,"endColumn":73},{"ruleId":"@typescript-eslint/no-misused-promises","severity":2,"message":"Promise returned in function argument where a void return was expected.","line":195,"column":31,"nodeType":"ArrowFunctionExpression","messageId":"voidReturnArgument","endLine":256,"endColumn":14},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an error typed value.","line":213,"column":39,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":217,"endColumn":35},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .canceled on a type that cannot be resolved.","line":218,"column":44,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":218,"endColumn":52},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .filePath on a type that cannot be resolved.","line":218,"column":64,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":218,"endColumn":72},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type error typed assigned to a parameter of type `string`.","line":223,"column":79,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":223,"endColumn":94},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .filePath on a type that cannot be resolved.","line":223,"column":86,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":223,"endColumn":94},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of a type that could not be resolved.","line":224,"column":39,"nodeType":"MemberExpression","messageId":"errorCall","endLine":224,"endColumn":71},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type error typed assigned to a parameter of type `PathOrFileDescriptor`.","line":225,"column":57,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":225,"endColumn":72},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .filePath on a type that cannot be resolved.","line":225,"column":64,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":225,"endColumn":72},{"ruleId":"obsidianmd/no-unsupported-api","severity":2,"message":"'FileManager.trashFile' requires Obsidian v1.6.6, but minAppVersion is 1.4.0.","line":228,"column":43,"nodeType":"MemberExpression","messageId":"apiNotAvailable","endLine":228,"endColumn":68},{"ruleId":"obsidianmd/no-unsupported-api","severity":2,"message":"'FileManager.trashFile' requires Obsidian v1.6.6, but minAppVersion is 1.4.0.","line":245,"column":35,"nodeType":"MemberExpression","messageId":"apiNotAvailable","endLine":245,"endColumn":60}],"suppressedMessages":[],"errorCount":15,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { TFile, TFolder, Notice, normalizePath, FileSystemAdapter, Menu, TAbstractFile, MenuItem } from 'obsidian';\nimport { IAlchemistModule, AlchemistContext } from '../../core/IAlchemistModule';\nimport { AlchemistSettings } from '../../settings';\nimport { getFfmpegArgs, AudioMetadata } from './logic';\n\nexport class AudioModule implements IAlchemistModule {\n public id = 'audio-converter';\n private context!: AlchemistContext;\n private statusBarItem: HTMLElement | null = null;\n\n async load(context: AlchemistContext): Promise<void> {\n this.context = context;\n this.statusBarItem = this.context.plugin.addStatusBarItem();\n if (this.statusBarItem) this.statusBarItem.hide();\n this.registerEvents();\n }\n\n async unload(): Promise<void> {\n // Events are cleaned up automatically\n }\n\n async onSettingsChange(newSettings: AlchemistSettings): Promise<void> {\n this.context.settings = newSettings;\n }\n\n private registerEvents() {\n const audioExtensions = ['webm', 'ogg', 'wav', 'mp3', 'm4a', 'flac', 'aac', 'opus', 'mp4', 'mov', 'mkv', 'avi'];\n\n // Handler for single file or folder\n this.context.plugin.registerEvent(\n this.context.app.workspace.on('file-menu', (menu: Menu, abstractFile: TAbstractFile) => {\n if (!this.context.settings.enableAudioConverter) return;\n const format = (this.context.settings.defaultAudioOutputFormat || 'mp3').toLowerCase();\n\n const audioFiles: TFile[] = [];\n\n if (abstractFile instanceof TFile) {\n const ext = abstractFile.extension.toLowerCase();\n if (audioExtensions.includes(ext) && ext !== format) {\n audioFiles.push(abstractFile);\n }\n } else if (abstractFile instanceof TFolder) {\n const collect = (f: TAbstractFile) => {\n if (f instanceof TFile) {\n const ext = f.extension.toLowerCase();\n if (audioExtensions.includes(ext) && ext !== format) {\n audioFiles.push(f);\n }\n } else if (f instanceof TFolder) {\n f.children.forEach((child: TAbstractFile) => collect(child));\n }\n };\n collect(abstractFile);\n }\n\n if (audioFiles.length === 0) return;\n\n menu.addItem((item: MenuItem) => {\n const title = audioFiles.length === 1 \n ? `Convert to ${format.toUpperCase()}` \n : `Convert folder (${audioFiles.length} files) to ${format.toUpperCase()}`;\n \n item\n .setTitle(title)\n .setIcon('music')\n .onClick(() => this.runConversion(audioFiles, format));\n });\n })\n );\n\n // Handler for multiple selection\n this.context.plugin.registerEvent(\n this.context.app.workspace.on('files-menu', (menu: Menu, files: TAbstractFile[]) => {\n if (!this.context.settings.enableAudioConverter) return;\n const format = (this.context.settings.defaultAudioOutputFormat || 'mp3').toLowerCase();\n\n const audioFiles = files.filter(f => \n f instanceof TFile && \n audioExtensions.includes(f.extension.toLowerCase()) && \n f.extension.toLowerCase() !== format\n ) as TFile[];\n\n if (audioFiles.length <= 1) return; // Single file handled by file-menu\n\n menu.addItem((item: MenuItem) => {\n item\n .setTitle(`Convert selected (${audioFiles.length} files) to ${format.toUpperCase()}`)\n .setIcon('music')\n .onClick(() => this.runConversion(audioFiles, format));\n });\n })\n );\n }\n\n\n\n private async runConversion(files: TFile[], format: string) {\n if (files.length === 0) return;\n \n const total = files.length;\n let processed = 0;\n\n if (this.statusBarItem) {\n this.statusBarItem.show();\n this.updateStatus(processed, total);\n }\n\n new Notice(`Converting ${total} file(s)...`);\n\n for (const file of files) {\n try {\n const metadata = await this.extractMetadata(file);\n await this.convert(file, format, metadata);\n processed++;\n this.updateStatus(processed, total);\n } catch (e) {\n console.error(`Alchemist Audio Error (${file.name}):`, e);\n new Notice(`Conversion failed for ${file.name}: ${(e as Error).message}`);\n }\n }\n\n if (this.statusBarItem) this.statusBarItem.hide();\n new Notice(`Finished processing ${processed}/${total} files.`);\n }\n\n private updateStatus(current: number, total: number) {\n if (!this.statusBarItem) return;\n this.statusBarItem.setText(`🧪 Alchemist: ${current}/${total} [${Math.round((current / total) * 100)}%]`);\n }\n\n private async extractMetadata(file: TFile): Promise<AudioMetadata> {\n const metadata: AudioMetadata = {\n title: file.basename,\n artist: this.context.settings.defaultAudioArtist,\n album: this.context.settings.defaultAudioAlbum,\n genre: this.context.settings.defaultAudioGenre,\n date: new Date().getFullYear().toString()\n };\n\n // Try to find a companion .md file with the same name\n const mdPath = file.path.replace(/\\.[^/.]+$/, \".md\");\n const mdFile = this.context.app.vault.getAbstractFileByPath(mdPath);\n\n if (mdFile instanceof TFile) {\n const cache = this.context.app.metadataCache.getFileCache(mdFile);\n const frontmatter = cache?.frontmatter;\n\n if (frontmatter) {\n if (frontmatter.title) metadata.title = String(frontmatter.title);\n if (frontmatter.artist) metadata.artist = String(frontmatter.artist);\n if (frontmatter.album) metadata.album = String(frontmatter.album);\n if (frontmatter.genre) metadata.genre = String(frontmatter.genre);\n \n // Date logic: date > creation_date > now\n if (frontmatter.date) metadata.date = String(frontmatter.date);\n else if (frontmatter.creation_date) metadata.date = String(frontmatter.creation_date);\n \n // Fallback for tags -> genre\n if (!frontmatter.genre && frontmatter.tags) {\n const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags : [frontmatter.tags];\n if (tags.length > 0) metadata.genre = String(tags[0]);\n }\n }\n\n // H1 Fallback for title\n if (!frontmatter?.title) {\n const content = await this.context.app.vault.read(mdFile);\n const h1Match = content.match(/^#\\s+(.+)$/m);\n if (h1Match) metadata.title = h1Match[1].trim();\n }\n }\n\n\n return metadata;\n }\n\n private async convert(file: TFile, format: string, metadata: AudioMetadata): Promise<void> {\n return new Promise(async (resolve, reject) => {\n const system = this.context.system;\n const app = this.context.app;\n const settings = this.context.settings;\n\n const adapter = app.vault.adapter as FileSystemAdapter;\n const basePath = adapter.getBasePath();\n const inputPath = system.path.join(basePath, file.path);\n \n const tempOutputPath = system.path.join(system.os.tmpdir(), `alchemist_${Date.now()}.${format}`);\n const args = getFfmpegArgs(inputPath, tempOutputPath, format, metadata);\n\n const child = system.spawn('ffmpeg', args);\n \n let stderr = '';\n child.stderr?.on('data', (data) => { stderr += data.toString(); });\n\n child.on('close', async (code) => {\n if (code === 0) {\n try {\n const data = system.fs.readFileSync(tempOutputPath);\n const outputName = file.basename + '.' + format;\n let finalVaultPath: string;\n\n switch (settings.targetFolderStrategy) {\n case 'specific': {\n const folder = settings.specificTargetFolder || '';\n finalVaultPath = normalizePath(`${folder}/${outputName}`);\n if (folder) {\n const existing = app.vault.getAbstractFileByPath(folder);\n if (!existing) await app.vault.createFolder(folder);\n }\n break;\n }\n case 'dialog': {\n const result = await system.showSaveDialog({\n title: 'Save converted audio',\n defaultPath: system.path.join(settings.lastDialogPath, outputName),\n filters: [{ name: format.toUpperCase(), extensions: [format] }]\n });\n if (result.canceled || !result.filePath) {\n system.fs.unlinkSync(tempOutputPath);\n resolve();\n return;\n }\n settings.lastDialogPath = system.path.dirname(result.filePath);\n await this.context.plugin.saveSettings();\n system.fs.writeFileSync(result.filePath, data);\n system.fs.unlinkSync(tempOutputPath);\n if (settings.deleteOriginalWebM && file.extension === 'webm') {\n await app.fileManager.trashFile(file);\n }\n resolve();\n return;\n }\n default: // 'source'\n finalVaultPath = file.path.replace(new RegExp(`\\\\.${file.extension}$`), `.${format}`);\n }\n \n const existingFile = app.vault.getAbstractFileByPath(finalVaultPath);\n if (existingFile instanceof TFile) {\n await app.vault.modifyBinary(existingFile, data.buffer);\n } else {\n await app.vault.createBinary(finalVaultPath, data.buffer);\n }\n\n if (settings.deleteOriginalWebM && file.extension === 'webm') {\n await app.fileManager.trashFile(file);\n }\n \n system.fs.unlinkSync(tempOutputPath);\n resolve();\n } catch (e) {\n reject(e instanceof Error ? e : new Error(String(e)));\n }\n } else {\n reject(new Error(`FFmpeg failed: ${stderr}`));\n }\n });\n\n child.on('error', (err) => reject(err));\n });\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/web/obsidian/new/src/plugins/alchemist/src/features/audio-converter/logic.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/web/obsidian/new/src/plugins/alchemist/src/features/dataview-export/DataviewModule.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":59,"column":15,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":59,"endColumn":58},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":59,"column":46,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":59,"endColumn":49,"fix":{"range":[2048,2051],"text":"unknown"},"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[2048,2051],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[2048,2051],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .plugins on an `any` value.","line":59,"column":51,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":59,"endColumn":58},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":60,"column":15,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":60,"endColumn":75},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of an `any` typed value.","line":60,"column":35,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":60,"endColumn":63},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .enabledPlugins on an `any` value.","line":60,"column":44,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":60,"endColumn":58},{"ruleId":"@typescript-eslint/no-floating-promises","severity":2,"message":"Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.","line":78,"column":13,"nodeType":"ExpressionStatement","messageId":"floatingVoid","endLine":78,"endColumn":41,"suggestions":[{"messageId":"floatingFixVoid","fix":{"range":[2884,2884],"text":"void "},"desc":"Add void operator to ignore."},{"messageId":"floatingFixAwait","fix":{"range":[2884,2884],"text":"await "},"desc":"Add await operator."}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an error typed value.","line":122,"column":19,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":126,"endColumn":15},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .canceled on a type that cannot be resolved.","line":128,"column":25,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":128,"endColumn":33},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .filePath on a type that cannot be resolved.","line":128,"column":44,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":128,"endColumn":52},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type error typed assigned to a parameter of type `string`.","line":129,"column":89,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":129,"endColumn":104},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .filePath on a type that cannot be resolved.","line":129,"column":96,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":129,"endColumn":104},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of a type that could not be resolved.","line":130,"column":23,"nodeType":"MemberExpression","messageId":"errorCall","endLine":130,"endColumn":55},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type error typed assigned to a parameter of type `PathOrFileDescriptor`.","line":131,"column":54,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":131,"endColumn":69},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .filePath on a type that cannot be resolved.","line":131,"column":61,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":131,"endColumn":69}],"suppressedMessages":[],"errorCount":15,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":1,"fixableWarningCount":0,"source":"import { Notice, TFolder, normalizePath } from 'obsidian';\nimport { IAlchemistModule, AlchemistContext } from '../../core/IAlchemistModule';\nimport { AlchemistSettings } from '../../settings';\nimport { escapeCsvCell } from './logic';\n\nexport class DataviewModule implements IAlchemistModule {\n public id = 'dataview-export';\n private context!: AlchemistContext;\n private observer: MutationObserver | null = null;\n private injectTimeout: number | null = null;\n\n async load(context: AlchemistContext): Promise<void> {\n this.context = context;\n this.setupGlobalObserver();\n this.start();\n }\n\n async unload(): Promise<void> {\n this.stop();\n }\n\n async onSettingsChange(newSettings: AlchemistSettings): Promise<void> {\n const wasEnabled = this.context.settings.enableDataviewExport;\n this.context.settings = newSettings;\n \n if (wasEnabled !== newSettings.enableDataviewExport) {\n if (newSettings.enableDataviewExport) this.start();\n else this.stop();\n }\n }\n\n private setupGlobalObserver() {\n this.observer = new MutationObserver(() => {\n if (!this.context.settings.enableDataviewExport) return;\n \n if (this.injectTimeout) activeWindow.clearTimeout(this.injectTimeout);\n this.injectTimeout = activeWindow.setTimeout(() => {\n this.injectAll();\n }, 300);\n });\n }\n\n private start() {\n if (this.context.settings.enableDataviewExport && this.observer) {\n this.observer.observe(activeDocument.body, { childList: true, subtree: true });\n this.injectAll();\n }\n }\n\n private stop() {\n if (this.observer) {\n this.observer.disconnect();\n }\n if (this.injectTimeout) activeWindow.clearTimeout(this.injectTimeout);\n activeDocument.querySelectorAll('.alchemist-export-btn').forEach(btn => btn.remove());\n }\n\n private injectAll() {\n const plugins = (this.context.app as any).plugins;\n const isDataviewEnabled = plugins?.enabledPlugins?.has('dataview');\n if (!isDataviewEnabled) return;\n\n const containers = activeDocument.querySelectorAll('.block-language-dataview, .dataview.table-view-table, .dataview-container');\n containers.forEach(c => this.injectButton(c as HTMLElement));\n }\n\n private injectButton(container: HTMLElement) {\n if (container.querySelector('.alchemist-export-btn')) return;\n if (!container.querySelector('table') && !container.classList.contains('block-language-dataview')) return;\n\n const btn = createEl('button');\n btn.className = 'alchemist-export-btn clickable-icon';\n btn.innerText = '💾 CSV';\n \n btn.onclick = (e) => {\n e.preventDefault();\n e.stopPropagation();\n this.exportTable(container);\n };\n\n container.appendChild(btn);\n if (getComputedStyle(container).position === 'static') {\n container.classList.add('alchemist-relative-container');\n }\n }\n\n private async exportTable(el: HTMLElement) {\n const table = el.querySelector('table');\n if (!table) {\n new Notice('No table found to export');\n return;\n }\n\n const rows = Array.from(table.querySelectorAll('tr'));\n const csvRows = rows.map(row => {\n const cells = Array.from(row.querySelectorAll('th, td'));\n return cells.map(cell => escapeCsvCell((cell as HTMLElement).innerText)).join(',');\n });\n\n const sourceFile = this.context.app.workspace.getActiveFile();\n csvRows.push(''); // Empty row separation\n csvRows.push(`\"Source\",\"[[${sourceFile?.path || 'unknown'}]]\"`);\n csvRows.push(`\"Exported\",\"${new Date().toLocaleString()}\"`);\n \n await this.handleSave(csvRows.join('\\n'));\n }\n\n private async handleSave(content: string) {\n const settings = this.context.settings;\n \n if (settings.dataviewExportStrategy === 'vault') {\n const folderPath = normalizePath(settings.dataviewVaultPath || 'Exports');\n await this.ensureFolder(folderPath);\n \n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n const fileName = `dataview_export_${timestamp}.csv`;\n const fullPath = `${folderPath}/${fileName}`;\n \n await this.context.app.vault.create(fullPath, content);\n new Notice(`CSV saved to vault: ${fullPath}`);\n } else {\n const result = await this.context.system.showSaveDialog({\n title: 'Export dataview to CSV',\n defaultPath: this.context.system.path.join(this.context.settings.lastDialogPath, 'dataview_export.csv'),\n filters: [{ name: 'CSV', extensions: ['csv'] }]\n });\n\n if (!result.canceled && result.filePath) {\n this.context.settings.lastDialogPath = this.context.system.path.dirname(result.filePath);\n await this.context.plugin.saveSettings();\n this.context.system.fs.writeFileSync(result.filePath, content);\n new Notice('CSV exported successfully');\n }\n }\n }\n\n private async ensureFolder(path: string) {\n const folder = this.context.app.vault.getAbstractFileByPath(path);\n if (folder instanceof TFolder) return;\n \n await this.context.app.vault.createFolder(path);\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/web/obsidian/new/src/plugins/alchemist/src/features/dataview-export/logic.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/web/obsidian/new/src/plugins/alchemist/src/features/paste-cleaner/SmartPasteModule.ts","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'htmlToMarkdown' is defined but never used. Allowed unused vars must match /^_/u.","line":1,"column":18,"nodeType":"Identifier","messageId":"unusedVar","endLine":1,"endColumn":32,"suggestions":[{"messageId":"removeUnusedVar","data":{"varName":"htmlToMarkdown"},"fix":{"range":[15,31],"text":""},"desc":"Remove unused variable \"htmlToMarkdown\"."}]},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'containsTrackers' is defined but never used. Allowed unused vars must match /^_/u.","line":4,"column":10,"nodeType":"Identifier","messageId":"unusedVar","endLine":4,"endColumn":26,"suggestions":[{"messageId":"removeUnusedVar","data":{"varName":"containsTrackers"},"fix":{"range":[202,219],"text":""},"desc":"Remove unused variable \"containsTrackers\"."}]},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'purifyHtmlStructure' is defined but never used. Allowed unused vars must match /^_/u.","line":4,"column":43,"nodeType":"Identifier","messageId":"unusedVar","endLine":4,"endColumn":62,"suggestions":[{"messageId":"removeUnusedVar","data":{"varName":"purifyHtmlStructure"},"fix":{"range":[233,254],"text":""},"desc":"Remove unused variable \"purifyHtmlStructure\"."}]}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { Editor, htmlToMarkdown, Notice } from 'obsidian';\nimport { IAlchemistModule, AlchemistContext } from '../../core/IAlchemistModule';\nimport { AlchemistSettings } from '../../settings';\nimport { containsTrackers, stripTrackers, purifyHtmlStructure } from './logic';\n\nexport class SmartPasteModule implements IAlchemistModule {\n public id = 'smart-paste';\n private context!: AlchemistContext;\n\n async load(context: AlchemistContext): Promise<void> {\n this.context = context;\n this.registerPasteHandler();\n this.registerCommands();\n }\n\n async unload(): Promise<void> {\n // Commands and events are automatically cleaned up if registered via plugin\n }\n\n async onSettingsChange(newSettings: AlchemistSettings): Promise<void> {\n this.context.settings = newSettings;\n }\n\n private registerCommands() {\n this.context.plugin.addCommand({\n id: 'clean-trackers-in-document',\n name: 'Clean trackers in current document',\n editorCallback: (editor: Editor) => {\n const content = editor.getValue();\n const cleaned = stripTrackers(content);\n if (content !== cleaned) {\n editor.setValue(cleaned);\n new Notice('Document cleaned from trackers');\n } else {\n new Notice('No trackers found');\n }\n }\n });\n }\n\n private registerPasteHandler() {\n this.context.plugin.registerEvent(\n this.context.app.workspace.on('editor-paste', async (evt: ClipboardEvent, editor: Editor) => {\n if (evt.defaultPrevented) return;\n if (!this.context.settings.enableSmartPaste) return;\n\n const clipboardData = evt.clipboardData;\n if (!clipboardData) return;\n\n const text = clipboardData.getData('text/plain')?.trim() || '';\n\n // SURGICAL INTERCEPTION: Only intercept pure URLs\n const urlRegex = /^(https?:\\/\\/[^\\s\"'>]+)$/;\n if (text.match(urlRegex) && this.context.settings.stripTrackingParameters) {\n const cleanUrl = stripTrackers(text);\n if (cleanUrl !== text) {\n evt.preventDefault();\n editor.replaceSelection(cleanUrl);\n new Notice('URL cleaned');\n }\n }\n \n // Note: We no longer intercept rich HTML pastes to avoid breaking \n // Obsidian's native formatting (like code blocks or auto-linking).\n // Users can use the \"Clean trackers in current document\" command instead.\n })\n );\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/web/obsidian/new/src/plugins/alchemist/src/features/paste-cleaner/logic.ts","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'e' is defined but never used. Allowed unused caught errors must match /^_/u.","line":45,"column":18,"nodeType":"Identifier","messageId":"unusedVar","endLine":45,"endColumn":19}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Pure logic for Smart Paste cleaning.\n * Zero dependencies on Obsidian API.\n */\n\nconst BLACKLIST = [\n 'utm_', 'fbclid', 'gclid', 'yclid', 'msclkid', 'mc_cid', 'mc_eid', \n '_hsenc', '_hsmi', 'sca_esv', 'sxsrf', 'ei', 'biw', 'bih', 'oq', \n 'gs_lp', 'sclient', 'ved', 'uact'\n];\n\n/**\n * Checks if a string contains any tracking parameters.\n */\nexport function containsTrackers(input: string): boolean {\n if (!input) return false;\n return BLACKLIST.some(b => input.toLowerCase().includes(b));\n}\n\n/**\n * Strips tracking parameters from all URLs found in the text.\n */\nexport function stripTrackers(input: string): string {\n const urlRegex = /(https?:\\/\\/[^\\s\"'>]+)/g;\n\n return input.replace(urlRegex, (url) => {\n try {\n // Handle HTML encoded entities\n const decodedUrl = url.replace(/&amp;/g, '&');\n const urlObj = new URL(decodedUrl);\n const params = urlObj.searchParams;\n \n const toRemove: string[] = [];\n params.forEach((_, key) => {\n if (BLACKLIST.some(b => key.toLowerCase().startsWith(b))) {\n toRemove.push(key);\n }\n });\n\n if (toRemove.length > 0) {\n toRemove.forEach(k => params.delete(k));\n return urlObj.toString();\n }\n return url;\n } catch (e) {\n return url;\n }\n });\n}\n\n/**\n * Sanitizes HTML content structure and removes invisible trackers.\n * (Note: Uses DOMParser which is available in Obsidian/Electron but not in pure Node)\n */\nexport function purifyHtmlStructure(html: string): string {\n // Structural Cleaning (MS Office / Google Docs noise)\n let content = html;\n \n // Remove zero-width spaces\n content = content.replace(/[\\u200B-\\u200D\\uFEFF]/g, '');\n \n // Clean specific junk IDs and styles\n content = content.replace(/\\sid=\"docs-internal-guid-[^\"]*\"/g, '');\n content = content.replace(/\\sstyle=\"[^\"]*\"/g, '');\n \n // Remove double spaces and non-breaking spaces\n content = content.replace(/&nbsp;/g, ' ').replace(/\\s\\s+/g, ' ');\n\n return content.trim();\n}\n","usedDeprecatedRules":[]},{"filePath":"/web/obsidian/new/src/plugins/alchemist/src/features/textbundle/TextBundleModule.ts","messages":[{"ruleId":"obsidianmd/ui/sentence-case","severity":2,"message":"Use sentence case for UI text. Expected: 'No Markdown files found in this folder.'","line":100,"column":48,"nodeType":"Literal","messageId":"useSentenceCase","endLine":100,"endColumn":89},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an error typed value.","line":105,"column":39,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":109,"endColumn":35},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .canceled on a type that cannot be resolved.","line":111,"column":45,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":111,"endColumn":53},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .filePaths on a type that cannot be resolved.","line":111,"column":64,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":111,"endColumn":73},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an error typed value.","line":112,"column":43,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":112,"endColumn":74},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .filePaths on a type that cannot be resolved.","line":112,"column":62,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":112,"endColumn":71},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an error typed value.","line":113,"column":37,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":113,"endColumn":85},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of a type that could not be resolved.","line":114,"column":43,"nodeType":"MemberExpression","messageId":"errorCall","endLine":114,"endColumn":75},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type error typed assigned to a parameter of type `string`.","line":123,"column":92,"nodeType":"Identifier","messageId":"unsafeArgument","endLine":123,"endColumn":101},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an error typed value.","line":150,"column":15,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":155,"endColumn":11},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .canceled on a type that cannot be resolved.","line":157,"column":21,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":157,"endColumn":29},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .filePaths on a type that cannot be resolved.","line":157,"column":40,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":157,"endColumn":49},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an error typed value.","line":158,"column":19,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":158,"endColumn":49},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .filePaths on a type that cannot be resolved.","line":158,"column":37,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":158,"endColumn":46},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type error typed assigned to a parameter of type `string`.","line":159,"column":85,"nodeType":"Identifier","messageId":"unsafeArgument","endLine":159,"endColumn":93},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of a type that could not be resolved.","line":160,"column":19,"nodeType":"MemberExpression","messageId":"errorCall","endLine":160,"endColumn":51},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an error typed value.","line":161,"column":19,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":161,"endColumn":126},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of a type that could not be resolved.","line":161,"column":32,"nodeType":"MemberExpression","messageId":"errorCall","endLine":161,"endColumn":70},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of a type that could not be resolved.","line":161,"column":32,"nodeType":"MemberExpression","messageId":"errorCall","endLine":161,"endColumn":59},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of a type that could not be resolved.","line":161,"column":32,"nodeType":"MemberExpression","messageId":"errorCall","endLine":161,"endColumn":46},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .split on a type that cannot be resolved.","line":161,"column":41,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":161,"endColumn":46},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .pop on a type that cannot be resolved.","line":161,"column":56,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":161,"endColumn":59},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .replace on a type that cannot be resolved.","line":161,"column":63,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":161,"endColumn":70},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type error typed assigned to a parameter of type `PathOrFileDescriptor`.","line":164,"column":66,"nodeType":"Identifier","messageId":"unsafeArgument","endLine":164,"endColumn":74},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type error typed assigned to a parameter of type `string | undefined`.","line":165,"column":65,"nodeType":"Identifier","messageId":"unsafeArgument","endLine":165,"endColumn":75},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an error typed value.","line":180,"column":19,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":184,"endColumn":15},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .canceled on a type that cannot be resolved.","line":186,"column":25,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":186,"endColumn":33},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .filePath on a type that cannot be resolved.","line":186,"column":44,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":186,"endColumn":52},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type error typed assigned to a parameter of type `string`.","line":187,"column":89,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":187,"endColumn":104},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .filePath on a type that cannot be resolved.","line":187,"column":96,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":187,"endColumn":104},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of a type that could not be resolved.","line":188,"column":23,"nodeType":"MemberExpression","messageId":"errorCall","endLine":188,"endColumn":55},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type error typed assigned to a parameter of type `PathOrFileDescriptor`.","line":189,"column":54,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":189,"endColumn":69},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .filePath on a type that cannot be resolved.","line":189,"column":61,"nodeType":"Identifier","messageId":"errorMemberExpression","endLine":189,"endColumn":69}],"suppressedMessages":[],"errorCount":33,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { TFile, TFolder, Notice, Menu, TAbstractFile, MenuItem } from 'obsidian';\nimport { IAlchemistModule, AlchemistContext } from '../../core/IAlchemistModule';\nimport { AlchemistSettings } from '../../settings';\nimport { TextBundleFeature } from './collector';\nimport { TextBundlePacker } from './packer';\nimport { TextBundleImporter } from './importer';\n\nexport class TextBundleModule implements IAlchemistModule {\n public id = 'textbundle';\n private context!: AlchemistContext;\n \n private collector!: TextBundleFeature;\n private packer!: TextBundlePacker;\n private importer!: TextBundleImporter;\n\n async load(context: AlchemistContext): Promise<void> {\n this.context = context;\n this.collector = new TextBundleFeature(context.app, context.settings);\n this.packer = new TextBundlePacker(context.app);\n this.importer = new TextBundleImporter(context.app, context.settings, context.system);\n\n this.registerCommands();\n this.registerEvents();\n this.registerRibbonIcons();\n }\n\n async unload(): Promise<void> {\n // Clean up if necessary\n }\n\n async onSettingsChange(newSettings: AlchemistSettings): Promise<void> {\n this.context.settings = newSettings;\n }\n\n private registerRibbonIcons() {\n this.context.plugin.addRibbonIcon('package-plus', 'Import textbundle', () => {\n if (!this.context.settings.enableTextBundle) {\n new Notice('Textbundle module is disabled in settings.');\n return;\n }\n void this.runImport();\n });\n }\n\n private registerCommands() {\n this.context.plugin.addCommand({\n id: 'import-textbundle',\n name: 'Import textbundle',\n callback: () => {\n if (!this.context.settings.enableTextBundle) return;\n void this.runImport();\n }\n });\n\n this.context.plugin.addCommand({\n id: 'export-current-textbundle',\n name: 'Export current file as textbundle',\n checkCallback: (checking: boolean) => {\n if (!this.context.settings.enableTextBundle) return false;\n const activeFile = this.context.app.workspace.getActiveFile();\n if (activeFile) {\n if (!checking) {\n void this.runExport(activeFile);\n }\n return true;\n }\n return false;\n }\n });\n }\n\n private registerEvents() {\n this.context.plugin.registerEvent(\n this.context.app.workspace.on('file-menu', (menu: Menu, abstractFile: TAbstractFile) => {\n if (!this.context.settings.enableTextBundle) return;\n\n if (abstractFile instanceof TFile && abstractFile.extension === 'md') {\n menu.addItem((item: MenuItem) => {\n item\n .setTitle('Export as textbundle')\n .setIcon('package')\n .onClick(() => {\n void this.runExport(abstractFile);\n });\n });\n } else if (abstractFile instanceof TFolder) {\n menu.addItem((item: MenuItem) => {\n item\n .setTitle('Export folder as textbundle')\n .setIcon('package')\n .onClick(async () => {\n const files: TFile[] = [];\n const collect = (f: TAbstractFile) => {\n if (f instanceof TFile && f.extension === 'md') files.push(f);\n else if (f instanceof TFolder) f.children.forEach(collect);\n };\n collect(abstractFile);\n \n if (files.length === 0) {\n new Notice('No markdown files found in this folder.');\n return;\n }\n\n // Bulk Export Strategy: Ask for a folder once\n const result = await this.context.system.showOpenDialog({\n title: 'Select destination for bulk export',\n defaultPath: this.context.settings.lastDialogPath,\n properties: ['openDirectory', 'createDirectory']\n });\n\n if (!result.canceled && result.filePaths.length > 0) {\n const targetDir = result.filePaths[0];\n this.context.settings.lastDialogPath = targetDir;\n await this.context.plugin.saveSettings();\n\n new Notice(`Bulk exporting ${files.length} notes...`);\n let successCount = 0;\n for (const f of files) {\n try {\n const context = await this.collector.collectResources(f);\n const blob = await this.packer.pack(context);\n const extension = this.context.settings.compressionFormat || 'textbundle';\n const fullPath = this.context.system.path.join(targetDir, `${f.basename}.${extension}`);\n \n this.context.system.fs.writeFileSync(fullPath, Buffer.from(blob));\n successCount++;\n } catch (e) {\n console.error(`Export failed for ${f.name}:`, e);\n }\n }\n new Notice(`Successfully exported ${successCount}/${files.length} notes to ${targetDir}`);\n }\n });\n });\n\n menu.addItem((item: MenuItem) => {\n item\n .setTitle('Import into here')\n .setIcon('package-plus')\n .onClick(() => {\n void this.runImport(abstractFile.path);\n });\n });\n }\n })\n );\n }\n\n private async runImport(targetPath?: string) {\n const result = await this.context.system.showOpenDialog({\n title: targetPath ? `Import into ${targetPath}` : 'Import textbundle',\n defaultPath: this.context.settings.lastDialogPath,\n filters: [{ name: 'Textbundle', extensions: ['textbundle', 'zip', 'textpack'] }],\n properties: ['openFile']\n });\n\n if (!result.canceled && result.filePaths.length > 0) {\n const filePath = result.filePaths[0];\n this.context.settings.lastDialogPath = this.context.system.path.dirname(filePath);\n await this.context.plugin.saveSettings();\n const sourceName = filePath.split(/[\\\\/]/).pop()?.replace(/\\.(zip|textpack|textbundle)$/i, '') || 'Imported_Note';\n \n try {\n const data = this.context.system.fs.readFileSync(filePath);\n await this.importer.importZip(data, targetPath, sourceName);\n new Notice('Successfully imported textbundle');\n } catch (e) {\n console.error('Alchemist Import Error:', e);\n new Notice(`Import failed: ${(e as Error).message}`);\n }\n }\n }\n\n private async runExport(file: TFile) {\n try {\n const context = await this.collector.collectResources(file);\n const blob = await this.packer.pack(context);\n const extension = this.context.settings.compressionFormat || 'textbundle';\n\n const result = await this.context.system.showSaveDialog({\n title: 'Save textbundle',\n defaultPath: this.context.system.path.join(this.context.settings.lastDialogPath, `${file.basename}.${extension}`),\n filters: [{ name: 'Textbundle', extensions: [extension, 'zip'] }]\n });\n\n if (!result.canceled && result.filePath) {\n this.context.settings.lastDialogPath = this.context.system.path.dirname(result.filePath);\n await this.context.plugin.saveSettings();\n this.context.system.fs.writeFileSync(result.filePath, Buffer.from(blob));\n new Notice(`Successfully exported: ${file.basename}`);\n }\n } catch (e) {\n console.error('Alchemist Export Error:', e);\n new Notice(`Export failed: ${(e as Error).message}`);\n }\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/web/obsidian/new/src/plugins/alchemist/src/features/textbundle/collector.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/web/obsidian/new/src/plugins/alchemist/src/features/textbundle/importer.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":89,"column":23,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":89,"endColumn":90},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'match' is defined but never used. Allowed unused args must match /^_/u.","line":130,"column":51,"nodeType":"Identifier","messageId":"unusedVar","endLine":130,"endColumn":56},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":217,"column":15,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":217,"endColumn":44},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":217,"column":41,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":217,"endColumn":44,"fix":{"range":[8572,8575],"text":"unknown"},"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[8572,8575],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[8572,8575],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .getConfig on an `any` value.","line":218,"column":26,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":218,"endColumn":35},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of an `any` typed value.","line":219,"column":21,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":219,"endColumn":36},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .getConfig on an `any` value.","line":219,"column":27,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":219,"endColumn":36}],"suppressedMessages":[],"errorCount":6,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":1,"fixableWarningCount":0,"source":"import { App, TFile, normalizePath, Notice } from 'obsidian';\nimport * as fflate from 'fflate';\nimport { AlchemistSettings } from '../../settings';\nimport { ISystemAdapter } from '../../core/IAlchemistModule';\nimport { reverseTransformLinks } from './logic';\n\ninterface TextBundleInfo {\n title?: string;\n origin_path?: string;\n}\n\nexport class TextBundleImporter {\n app: App;\n settings: AlchemistSettings;\n system: ISystemAdapter;\n\n constructor(app: App, settings: AlchemistSettings, system: ISystemAdapter) {\n this.app = app;\n this.settings = settings;\n this.system = system;\n }\n\n /**\n * Entry point for ZIP import.\n */\n async importZip(data: ArrayBuffer, targetPath?: string, sourceName?: string) {\n try {\n const unzipped = fflate.unzipSync(new Uint8Array(data));\n await this.processUnzipped(unzipped, targetPath, sourceName);\n new Notice('Import successful');\n } catch (e) {\n console.error('Alchemist: Import failed', e);\n new Notice('Import failed. Check console for details.');\n }\n }\n\n private async processUnzipped(unzipped: Record<string, Uint8Array>, targetPath?: string, sourceName?: string) {\n const allKeys = Object.keys(unzipped);\n \n // Identify bundles (.textbundle folders)\n const bundleFolders = new Set<string>();\n allKeys.forEach(k => {\n const match = k.match(/(.*\\.textbundle)\\//);\n if (match) bundleFolders.add(match[1]);\n });\n\n if (bundleFolders.size === 0) {\n // Flat structure\n await this.importSingleBundle(unzipped, targetPath, sourceName);\n } else {\n // TextPack structure\n new Notice(`Importing ${bundleFolders.size} notes from textpack...`);\n for (const folder of bundleFolders) {\n const prefix = `${folder}/`;\n const bundleFiles: Record<string, Uint8Array> = {};\n for (const key of allKeys) {\n if (key.startsWith(prefix)) {\n bundleFiles[key.replace(prefix, '')] = unzipped[key];\n }\n }\n const cleanName = folder.replace('.textbundle', '');\n await this.importSingleBundle(bundleFiles, targetPath, cleanName);\n }\n }\n }\n\n private async importSingleBundle(files: Record<string, Uint8Array>, targetPath?: string, sourceName?: string) {\n const allKeys = Object.keys(files);\n \n const textKey = allKeys.find(k => \n k.endsWith('text.md') || k.endsWith('text.markdown') || k.endsWith('text.txt') ||\n k === 'text.md' || k === 'text.markdown' || k === 'text.txt'\n );\n \n if (!textKey) return; \n \n const textData = files[textKey];\n let content = fflate.strFromU8(textData);\n \n const importRoot = targetPath || this.settings.defaultImportPath || '/';\n \n let noteName = sourceName ? `${sourceName}.md` : `Imported_Note.md`;\n let finalImportFolder = normalizePath(importRoot);\n\n // 1. Read info.json FIRST to determine structure\n const infoKey = allKeys.find(k => k === 'info.json');\n if (infoKey) {\n try {\n const info: TextBundleInfo = JSON.parse(fflate.strFromU8(files[infoKey]));\n if (info.title) noteName = `${info.title}.md`;\n \n if (info.origin_path && this.settings.restoreFolderStructure) {\n const relativeDir = this.system.path.dirname(info.origin_path);\n if (relativeDir !== '.') {\n finalImportFolder = normalizePath(`${importRoot}/${relativeDir}`);\n }\n }\n } catch {\n // Ignore parsing errors\n }\n }\n\n await this.ensureFolder(finalImportFolder);\n // Put assets in a central \"assets\" folder within the import root (or relative root)\n const assetsFolderName = 'assets';\n const attachmentFolderPath = normalizePath(`${finalImportFolder}/${assetsFolderName}`);\n await this.ensureFolder(attachmentFolderPath);\n\n const assetsBase = 'assets/';\n const assetRenameMap = new Map<string, string>();\n\n // 2. Import Assets\n for (const [path, assetData] of Object.entries(files)) {\n if (path.startsWith(assetsBase) && path !== assetsBase) {\n const originalFileName = path.replace(assetsBase, '');\n const actualFileName = await this.importAssetWithSafety(originalFileName, assetData, attachmentFolderPath);\n \n if (actualFileName !== originalFileName) {\n assetRenameMap.set(originalFileName, actualFileName);\n }\n }\n }\n\n // 3. Link Restoration\n content = reverseTransformLinks(content, assetsFolderName);\n \n for (const [oldName, newName] of assetRenameMap.entries()) {\n const escapedOldName = oldName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const linkRegex = new RegExp(`\\\\[\\\\[${assetsFolderName}/${escapedOldName}(\\\\|[^\\\\]]+)?\\\\]\\\\]`, 'g');\n content = content.replace(linkRegex, (match, alias) => {\n const aliasStr = alias as string | undefined;\n return `[[${assetsFolderName}/${newName}${aliasStr || ''}]]`;\n });\n }\n\n await this.createNoteWithSafety(finalImportFolder, noteName, content);\n }\n\n private async importAssetWithSafety(fileName: string, data: Uint8Array, folderPath: string): Promise<string> {\n await this.ensureFolder(folderPath);\n \n let actualName = fileName;\n let fullPath = normalizePath(`${folderPath}/${actualName}`);\n let counter = 1;\n\n const nameParts = fileName.split('.');\n const ext = nameParts.pop();\n const base = nameParts.join('.');\n\n while (await this.app.vault.adapter.exists(fullPath)) {\n const existingData = await this.app.vault.adapter.readBinary(fullPath);\n \n // If identical, reuse\n if (this.areBuffersEqual(new Uint8Array(existingData), data)) {\n return actualName;\n }\n\n // Otherwise rename\n actualName = `${base} (${counter}).${ext}`;\n fullPath = normalizePath(`${folderPath}/${actualName}`);\n counter++;\n }\n\n await this.app.vault.createBinary(fullPath, data.buffer as ArrayBuffer);\n return actualName;\n }\n\n private areBuffersEqual(buf1: Uint8Array, buf2: Uint8Array): boolean {\n if (buf1.length !== buf2.length) return false;\n for (let i = 0; i < buf1.length; i++) {\n if (buf1[i] !== buf2[i]) return false;\n }\n return true;\n }\n\n private async createNoteWithSafety(folderPath: string, fileName: string, content: string) {\n let finalPath = normalizePath(`${folderPath}/${fileName}`);\n let counter = 1;\n\n const nameParts = fileName.split('.');\n const ext = nameParts.pop();\n const base = nameParts.join('.');\n\n while (this.app.vault.getAbstractFileByPath(finalPath)) {\n if (this.settings.conflictStrategy === 'skip') return;\n if (this.settings.conflictStrategy === 'overwrite') {\n const existing = this.app.vault.getAbstractFileByPath(finalPath);\n if (existing instanceof TFile) {\n await this.app.vault.modify(existing, content);\n }\n return;\n }\n // Default or rename strategy\n finalPath = normalizePath(`${folderPath}/${base} (${counter}).${ext}`);\n counter++;\n }\n\n await this.app.vault.create(finalPath, content);\n }\n\n private async ensureFolder(path: string) {\n if (!path || path === '/' || path === '.') return;\n const normalized = normalizePath(path);\n if (this.app.vault.getAbstractFileByPath(normalized)) return;\n\n const parts = normalized.split('/');\n let currentPath = '';\n for (const part of parts) {\n currentPath = currentPath ? `${currentPath}/${part}` : part;\n if (!this.app.vault.getAbstractFileByPath(currentPath)) {\n await this.app.vault.createFolder(currentPath);\n }\n }\n }\n\n private getAttachmentFolder(): string {\n const vault = this.app.vault as any;\n if (typeof vault.getConfig === 'function') {\n return (vault.getConfig('attachmentFolderPath') as string) || '';\n }\n return '';\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/web/obsidian/new/src/plugins/alchemist/src/features/textbundle/logic.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":20,"column":15,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":20,"endColumn":46},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of an `any` typed value.","line":20,"column":31,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":20,"endColumn":41},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .split on an `any` value.","line":20,"column":36,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":20,"endColumn":41},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type `any` assigned to a parameter of type `string`.","line":21,"column":52,"nodeType":"Identifier","messageId":"unsafeArgument","endLine":21,"endColumn":56},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":31,"column":15,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":31,"endColumn":46},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of an `any` typed value.","line":31,"column":31,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":31,"endColumn":41},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .split on an `any` value.","line":31,"column":36,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":31,"endColumn":41},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type `any` assigned to a parameter of type `string`.","line":34,"column":47,"nodeType":"Identifier","messageId":"unsafeArgument","endLine":34,"endColumn":51},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type `any` assigned to a parameter of type `string`.","line":44,"column":49,"nodeType":"Identifier","messageId":"unsafeArgument","endLine":44,"endColumn":53},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'assetsPrefix' is assigned a value but never used. Allowed unused vars must match /^_/u.","line":64,"column":11,"nodeType":"Identifier","messageId":"unusedVar","endLine":64,"endColumn":23},{"ruleId":"no-useless-escape","severity":2,"message":"Unnecessary escape character: \\).","line":68,"column":58,"nodeType":"Literal","messageId":"unnecessaryEscape","endLine":68,"endColumn":59,"suggestions":[{"messageId":"removeEscape","fix":{"range":[2381,2382],"text":""},"desc":"Remove the `\\`. This maintains the current functionality."},{"messageId":"escapeBackslash","fix":{"range":[2381,2381],"text":"\\"},"desc":"Replace the `\\` with `\\\\` to include the actual backslash character."}]},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'match' is defined but never used. Allowed unused args must match /^_/u.","line":68,"column":70,"nodeType":"Identifier","messageId":"unusedVar","endLine":68,"endColumn":75},{"ruleId":"no-useless-escape","severity":2,"message":"Unnecessary escape character: \\).","line":74,"column":57,"nodeType":"Literal","messageId":"unnecessaryEscape","endLine":74,"endColumn":58,"suggestions":[{"messageId":"removeEscape","fix":{"range":[2723,2724],"text":""},"desc":"Remove the `\\`. This maintains the current functionality."},{"messageId":"escapeBackslash","fix":{"range":[2723,2723],"text":"\\"},"desc":"Replace the `\\` with `\\\\` to include the actual backslash character."}]},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'match' is defined but never used. Allowed unused args must match /^_/u.","line":74,"column":69,"nodeType":"Identifier","messageId":"unusedVar","endLine":74,"endColumn":74},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'match' is defined but never used. Allowed unused args must match /^_/u.","line":81,"column":91,"nodeType":"Identifier","messageId":"unusedVar","endLine":81,"endColumn":96},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type `any` assigned to a parameter of type `string`.","line":82,"column":50,"nodeType":"Identifier","messageId":"unsafeArgument","endLine":82,"endColumn":60}],"suppressedMessages":[],"errorCount":12,"fatalErrorCount":0,"warningCount":4,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Pure functions for TextBundle processing.\n * These functions have ZERO dependencies on Obsidian API.\n */\n\nexport interface LinkResolver {\n resolveAsset(link: string): string | null; // Returns bundle asset name if found\n resolveNote(link: string): string | null; // Returns relative path to other note if found\n}\n\n/**\n * Transforms Obsidian-style links [[link]] and ![[embed]] to Markdown-style [link](assets/link)\n */\nexport function transformMarkdownLinks(\n content: string, \n resolver: LinkResolver\n): string {\n // 1. Transform Embeds ![[link]] or ![[link|alias]]\n content = content.replace(/!\\[\\[([^\\]]+)\\]\\]/g, (match, link) => {\n const [path, alias] = link.split('|');\n const resolvedName = resolver.resolveAsset(path);\n \n if (resolvedName) {\n return `![${alias || resolvedName}](assets/${resolvedName})`;\n }\n return match;\n });\n\n // 2. Transform Regular Links [[link]] or [[link|alias]]\n content = content.replace(/\\[\\[([^\\]]+)\\]\\]/g, (match, link) => {\n const [path, alias] = link.split('|');\n \n // 1. Try resolving as another note in TextPack FIRST\n const notePath = resolver.resolveNote(path);\n if (notePath) {\n const parts = notePath.split('/');\n const bundleName = parts[parts.length - 2] || '';\n const decodedBundle = decodeURIComponent(bundleName);\n const basename = decodedBundle.replace('.textbundle', '').split(' - ').pop() || 'Note';\n return `[${alias || basename}](${notePath})`;\n }\n\n // 2. Try resolving as asset\n const assetName = resolver.resolveAsset(path);\n if (assetName) {\n return `[${alias || assetName}](assets/${assetName})`;\n }\n\n return match;\n });\n\n return content;\n}\n\n/**\n * Transforms Markdown-style links [link](assets/link) back to Obsidian-style [[link]]\n * @param content Markdown content\n * @param assetsFolder The folder where assets were imported into (e.g. 'media')\n */\nexport function reverseTransformLinks(\n content: string, \n assetsFolder: string\n): string {\n const assetsPrefix = 'assets/';\n const targetPrefix = assetsFolder ? `${assetsFolder}/` : '';\n\n // 1. Embeds: ![alt](assets/filename) -> ![[targetPrefix/filename|alt]]\n content = content.replace(/!\\[([^\\]]*)\\]\\(assets\\/([^\\)]+)\\)/g, (match, alt, filename) => {\n const isRedundant = alt === filename;\n return (alt && !isRedundant) ? `![[${targetPrefix}${filename}|${alt}]]` : `![[${targetPrefix}${filename}]]`;\n });\n\n // 2. Links: [text](assets/filename) -> [[targetPrefix/filename|text]]\n content = content.replace(/\\[([^\\]]+)\\]\\(assets\\/([^\\)]+)\\)/g, (match, text, filename) => {\n const isRedundant = text === filename;\n return (text && !isRedundant) ? `[[${targetPrefix}${filename}|${text}]]` : `[[${targetPrefix}${filename}]]`;\n });\n\n\n // 3. Cross-Bundle Links: [Label](../BundleName.textbundle/text.md) -> [[NoteName|Label]]\n content = content.replace(/\\[([^\\]]+)\\]\\(\\.\\.\\/([^/]+)\\/text\\.(md|markdown|txt)\\)/g, (match, text, folderName) => {\n const decodedBundle = decodeURIComponent(folderName);\n const bundleBase = decodedBundle.replace(/\\.textbundle$/i, '');\n // Handle collision suffixes like \"Note (1)\"\n const noteName = bundleBase.replace(/ \\(\\d+\\)$/, '');\n \n return text === noteName ? `[[${noteName}]]` : `[[${noteName}|${text}]]`;\n });\n\n return content;\n}\n\n","usedDeprecatedRules":[]},{"filePath":"/web/obsidian/new/src/plugins/alchemist/src/features/textbundle/packer.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/web/obsidian/new/src/plugins/alchemist/src/main.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/web/obsidian/new/src/plugins/alchemist/src/settings.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]}]