image-gin

Maintenance: Update all packages, conform to Obsidian updates, support Magnific after rebrand from Freepik

Summary

This was the first session in this repo since adopting Claude Code. The repo had been dormant for ~7 months. Goals: refresh dependencies to current versions, get the build green under the new toolchain, restore the Freepik integration that had silently broken, and align it with Freepik's rebrand to Magnific.

1. Initialized CLAUDE.md

New file at the repo root with build commands, the architecture sketch (entry point at main.ts/root, modal-per-command pattern, services layer wrapping external APIs), and codebase-specific conventions distilled from .windsurfrules.md and observed code (use requestUrl not fetch, hand-rolled YAML frontmatter parser, manual multipart for ImageKit uploads, esbuild externals list, strict TS settings).

2. Dependency upgrades (pnpm update --latest)

PackageFromTo
typescript5.8.36.0.3
@types/node24.0.1225.6.0
eslint9.31.010.3.0
@typescript-eslint/*8.37.08.59.1
@eslint/plugin-kit0.3.30.7.1
esbuild0.25.60.28.0
obsidian1.8.71.12.3
builtin-modules5.0.05.1.0
@modelcontextprotocol/sdk1.15.01.29.0
fastify5.4.05.8.5
zod4.0.04.4.2

pnpm update also stripped a stray dev 0.1.3 package that had been sitting in dependencies — looked like an accidental pnpm add dev from an earlier session.

3. tsconfig.json migration for TypeScript 6

TS 6 deprecated moduleResolution: "node" (an alias for node10) and bare baseUrl. Changes:

  • moduleResolution: "node""bundler" (correct for esbuild-bundled projects).

  • Dropped baseUrl and paths. The paths entry only restated TypeScript's default node_modules lookup — it was a no-op.

  • Added "types": ["node"]. Under the old moduleResolution, TS auto-scanned node_modules/@types/* and globally included anything found, so Buffer, require, path, fs, child_process were all ambient. Under bundler resolution that auto-inclusion is gone — you opt in explicitly.

If a future @types/* package needs to provide ambient globals (e.g. @types/jest), it has to be added to the types array.

4. Type fixes flowing from the upgrade

  • src/services/imageCacheService.ts — fixed two pre-existing strict-TS errors that had been blocking the build before the upgrades were even attempted: unused Vault import, unused settings field. The unused-field fix wires the service to respect settings.imageCache.cacheFolder instead of a hardcoded path. Same default value, but the user-configurable setting now actually does something.

  • src/services/imageCacheService.ts + src/services/recraftImageService.ts — new @types/node types Uint8Array as Uint8Array<ArrayBufferLike>, which no longer satisfies Obsidian's ArrayBuffer parameter on vault.createBinary. Pass arrayBuffer directly in one spot; bytes.buffer as ArrayBuffer in the other (safe — new Uint8Array(length) always allocates a real ArrayBuffer).

5. CSS bundling fix (behavior change)

src/modals/FreepikModal.ts had import '../styles/freepik.css' that esbuild was silently ignoring on every prior build (with a warning). Result: those styles never reached users. Switched to @import './freepik.css' in the CSS entry point (current-file-modal.css) so the styles actually ship now. Bundle grew from 7.3kb → 9.2kb. (Later renamed to magnific.css — see §8.)

6. Workspace isolation (pnpm-workspace.yaml)

The parent lossless-monorepo defines a pnpm workspace at /Users/mpstaton/code/lossless-monorepo/pnpm-workspace.yaml. Even though image-gin is not in the parent's packages: array, pnpm 10 walks UP from any directory looking for a workspace root and operates on whatever it finds. Result: running pnpm install from inside image-gin would prompt to nuke node_modules in the parent and sibling projects (site/).

Fix: added pnpm-workspace.yaml (with packages: []) inside image-gin/. pnpm treats the directory containing this file as the workspace root and stops walking up. image-gin is now a fully self-contained pnpm root.

(Aside: .gitmodules only governs git submodules — it doesn't affect pnpm. The user's intuition pointed there first; the actual knob is the workspace yaml.)

7. Freepik service was structurally broken — rewrote with requestUrl

The Freepik integration silently returned no results for any search. Investigation showed freepikService.ts was implemented by shelling out to curl via child_process.exec — very different from how recraftImageService and imagekitService work (both use Obsidian's requestUrl).

Failure modes in the curl approach:

  • The promise rejected on any stderr output, even for successful requests.

  • No HTTP status check — non-2xx responses returned as if successful with empty data.

  • API key was interpolated directly into a shell command (would break on special characters, though the actual key was alphanumeric).

Rewrote searchImages to use requestUrl with throw: false and an explicit status check. This is what surfaced the real underlying problem: the stored Freepik API key was no longer valid (Freepik had rebranded to Magnific and reissued credentials).

8. Magnific rebrand — full rename

Freepik rebranded to Magnific. Their API is the same shape (same response JSON, same auth model — single API key in a header) but new host (api.magnific.com) and new header (x-magnific-api-key). The user obtained a Magnific API key.

Did a coordinated rename across the codebase:

  • File renames (via git mv): freepikService.tsmagnificService.ts, FreepikModal.tsMagnificModal.ts, freepik.cssmagnific.css.

  • Symbol renames: FreepikServiceMagnificService, FreepikImageMagnificImage, FreepikSearchResultMagnificSearchResult, FreepikModalMagnificModal, FreepikSettingsMagnificSettings.

  • Settings schema: settings.freepiksettings.magnific. Default license enum 'free' | 'premium''freemium' | 'premium' (Magnific's vocabulary).

  • Command rename: search-freepik-imagessearch-magnific-images. UI label Search Freepik ImagesSearch Magnific Images.

  • CSS classes: freepik-*magnific-* (search-container, grid, image-container, thumbnail, title, status, no-results, error-message, etc.). Updated @import in current-file-modal.css.

  • Logger filename: freepik-errors.jsonmagnific-errors.json.

  • Cache filename prefix: freepik_<hash>.<ext>magnific_<hash>.<ext>. Existing cached files become stale; they age out via the cache cleanup policy.

One-shot migration in loadSettings() copies any legacy freepik: {...} block from data.json into magnific: {...} and persists. Without this, every existing user's saved API key and enabled flag would have been orphaned by the schema rename and the integration would fail with "Magnific integration is not enabled" on first load. The migration shim is intentionally still labeled "freepik" inside the code — that's the legacy field name it's looking for. Safe to delete the shim in a future release once existing installs have all migrated.

Deliberately NOT changed: the plugin id (image-gin-plugin in manifest.json). Changing the id would orphan every existing install in Obsidian (different plugin folder, settings reset).

9. Cache-folder error noise fix

ImageCacheService.ensureCacheFolder() was logging Failed to create cache folder: Error: Folder already exists. on every plugin load. The default cache folder lives under .obsidian/plugins/image-gin/cache, and vault.getAbstractFileByPath() does not index anything under .obsidian/ — so the existence check returned null, then createFolder ran and Obsidian's adapter complained.

Switched to vault.adapter.exists + adapter.mkdir (which see real filesystem paths), and made the catch block swallow the specific "already exists" error message — race-safe even if multiple ImageCacheService instances try to mkdir simultaneously (which happens because both the Settings tab and MagnificModal construct one).

Verified working at end of session

  • pnpm build green.

  • Magnific image search returns results in Obsidian.

  • Settings UI shows Magnific section with Freemium/Premium dropdown.

  • One-shot settings migration carried over the user's existing config.

Known follow-ups (not done)

  • The defaultLicense setting is not yet sent in the search request. If license filtering is wanted, the request shape needs a follow-up.

  • The migration shim in loadSettings() can be removed in a future release once all existing installs have run it once.

  • obsidian is pinned to latest in devDependencies. Acceptable as-is; pin to a specific version if reproducible builds matter.

  • obsidian-git plugin (separate from this codebase) was logging git errors throughout testing — unrelated to image-gin, owned by that other plugin.