Obsidian moved community-plugin submission from the obsidian-releases PR
process to the community.obsidian.md developer portal. Replace the now-defunct
'I'm Not Dead Yet' PR-refresh procedure in CLAUDE.md with the portal-based
distribution reality (stable Latest release matching manifest, no v-prefix,
versions.json, make promote).
ADR-300 records the decision to keep a single main branch with the existing
prerelease/promote flow rather than add a dedicated Obsidian release branch:
for 'same code, slower cadence' the prerelease/promote gap already provides
the decoupling, and a release branch would only add permanent version-file
divergence. Rejected alternatives documented.
The plugin code already calls APIs introduced in newer Obsidian versions
(FileManager.trashFile in 1.6.6, Workspace.getLeaf in 0.16.0,
Vault.createFolder in 1.4.0, ToggleComponent.setDisabled in 1.2.3,
AbstractTextComponent.setDisabled in 1.2.3, ButtonComponent.setTooltip
in 1.1.0). Declaring minAppVersion 0.15.0 was misleading — anyone
between 0.15 and 1.5 would have the plugin load and then crash at
runtime. Set the floor to 1.6.6 to match the highest API actually used.
The 0.3.0 release adds typed rules (no-plugin-as-component, no-view-references-in-plugin,
no-unsupported-api, prefer-file-manager-trash-file, prefer-instanceof) and wires
them into the hybrid recommended config without a **/*.ts files scope. As a
result they also try to run against package.json — which uses ESLint's JSON
language and has no @typescript-eslint parser services — and ESLint aborts
the whole run with "Error while loading rule 'obsidianmd/no-plugin-as-component'".
- eslint.config.mts: disable those five typed rules on JSON files, and
ignore the data files the recommended config doesn't wire a JSON parser
for (tsconfig.json, package-lock.json, manifest.json, versions.json,
src/config/**/*.json, .claude/**).
- Keep the autofix for `setTimeout` → `window.setTimeout` and
`globalThis` → `window` in code paths that only run inside Obsidian
(main.ts, main-complex.ts, worker-manager.ts, connection-pool.ts,
tools/fetch.ts) — that's the popout-window-compat behaviour the rule
exists to enforce.
- Revert the autofix in code paths imported by Jest tests where `window`
is undefined: session-manager.ts and obsidian-api.ts background timers,
semantic/router.ts active-file timeout, debug.ts console reference.
Each retains a `// eslint-disable-next-line` with the reason
(background timer / Node test compat).
Closes#115. Closes#123.
The Dataview API returns Luxon DateTime objects (which expose toISO(), not
toISOString()) for file.ctime/mtime, so listPages() and getPageMetadata()
crashed with "toISOString is not a function" the moment any real Dataview
result reached them. The existing tests passed only because the mocks used
native Date, which exposes toISOString().
The query path had a separate envelope bug: executeQuery() hard-coded
success: true whenever dataviewAPI.query() didn't throw, while Dataview
itself returns { successful: false, error: "..." } for malformed queries
without throwing. The formatter then read response.successful (undefined),
hit the failure branch, and rendered every result as "❌ Query failed:
Unknown error" — even when the query had succeeded.
- Add toIsoOptional() that prefers Luxon's toISO() and falls back to
Date.toISOString(); apply at every page.file.ctime/mtime serialization
site and inside convertDataviewValue().
- Propagate result.successful and result.error from the inner Dataview
response to the outer tool envelope.
- Add a dataview.query normalizer case in the formatter dispatcher that
flattens result.{type,values,headers} to the top level and maps
success → successful, matching the formatter's contract.
- Regression tests for both bugs, including a Luxon-shaped mock distinct
from the existing Date-shaped one.
Pagination was silently a no-op for the MCP-facing surface. MCP
clients send numeric arguments (page, pageSize, snippetLength) as
JSON numbers; paramStr returns undefined for non-string values, so
parseInt(paramStr(...) ?? '1') always collapsed to defaults.
Caught while live-verifying the #158 pagination work shipped in
0.11.19: a call with page=2 pageSize=50 returned page 1 pageSize 20.
The router was discarding the user's values before the call ever
reached listFilesPaginated.
Three call sites fixed in src/semantic/router.ts:
- vault.list (line 174-175) — the immediate verifier
- vault.search (line 286-287)
- vault.search snippet length (line 306)
paramNum already existed at line 44; this commit just wires it in.
Code-reviewer caught a real bug: listFiles (recursive flat) sorts by
full path, but listFilesPaginated in recursive mode was sorting
folders-first-then-by-basename. So the agent's "use page=2 pageSize=50"
hint anchored to a path-sorted universe, but page 2 returned a
basename-sorted universe — overlaps and gaps for any caller trying to
walk the listing.
Recursive paginated mode now sorts by full path, matching listFiles.
Level-only mode (the default for internal callers — cp recursion,
isDirectory probe) keeps the folder-first-then-basename order it
needs for tree navigation.
New test (`paged universe matches the flat listFiles universe`)
concatenates every page and asserts equality with the flat call —
exactly the agent-facing contract the formatter's next-page hint
promises.
Router comment also clarifies why root paginated stays recursive=false
(getAllLoadedFiles is already recursive — the path collapses to the
same answer either way).
Refs PR #158 review.
When the formatter truncated a directory listing with "... and N more",
the agent had no usable next call. \`page=2 pageSize=50\` would have
returned a fundamentally different shape (level-only folder entries
from listFilesPaginated, not the next 50 files from the recursive
listFiles), so any hint to "paginate" would have led the agent
somewhere broken.
This change makes pagination semantically consistent and gives the
agent an explicit, working escape hatch:
- listFilesPaginated gains a recursive flag (default false to
preserve existing internal callers — cp at router.ts:1049 and the
isDirectory probe at router.ts:1021 still get level-only).
- vault.list passes recursive=true when paginating with a non-root
directory. Page N of vault.list('foo') now returns the Nth slice
of the same flat universe vault.list('foo') (unpaginated) would
return — no shape switch.
- The flat-list formatter, when it truncates at 50, now tells the
agent exactly what to call next: page=2 pageSize=50, or raw:true
for the full list in one response.
- The structured-response formatter shows "Page X of Y" and, when
there are more pages, surfaces the next-page call with the
current pageSize baked in.
- normalizeResponse gains a vault.list case translating the API
response's \`type: 'file'|'folder'\` to the formatter's \`isFolder\`
boolean. Previously the structured branch's filter on f.isFolder
always failed silently, lumping every entry into "Files" even when
half were folders.
Two new tests in tests/list-files-recursive.test.ts:
- recursive paginated mode produces the expected slice with no
cross-page overlap
- recursive=false (default) preserves the level-only behavior the
internal callers depend on
Refs #154 (extends the agent-facing fix).
vault.list(directory) returned an empty array whenever the target
folder's direct children were all subfolders — listFiles used
folder.children and then filtered to TFile, dropping every
descendant. Meanwhile the root case used vault.getAllLoadedFiles()
which is recursive, so root and directory listings diverged.
listFiles now walks the folder tree when a directory is provided,
matching the recursive semantics of the root case. Callers that
were already iterating result paths get the same shape — full
vault-rooted paths to .md files — they just no longer hit the
empty-list cliff for folder-of-folders inputs.
The synchronous throw on missing directory is preserved; existing
try/catch sites at router.ts:538 and :1030 use it as a "does this
exist as a directory" probe.
Adds tests/list-files-recursive.test.ts with regression coverage:
- recursing through a folder of subfolders returns all descendant files
- recurses through arbitrary nesting depth
- still throws on a missing directory
- root call (no directory) still returns the full vault
Test fixtures: tests/__mocks__/obsidian.ts gained a TFolder class
and a stat field on TFile.
Closes#154.
After merging the dependabot batch, npm audit still flagged 5
transitive vulns (4 high, 1 moderate). Running npm audit fix lifts
deep deps to patched ranges and resolves all of them — except it
tries to land yaml@2.9.0 which was published 2026-05-11, inside the
7-day hold window.
Added a package.json override to pin yaml to ~2.8.4 (published
2026-05-02, 13 days aged). 2.8.4 already fixes the CVE-flagged
"Stack Overflow via deeply nested YAML collections" (range
2.0.0-2.8.2), and we get the safety of an aged release.
Final state: 0 vulnerabilities, 127/127 tests still green, all
upgrades respect the 7-day rule.
npm supply-chain attacks have been spiking — compromised package
versions get published, ingested by automation, then yanked days
later. Adding a guideline that we only land upgrades to versions
≥7 days old to buy time for community discovery before pulling
fresh releases into the dependency tree.
Captured next to the existing pre-push quality checks since it's a
delivery-discipline rule, same shape as "run tests before pushing."
The release pipeline always emits prereleases (per the standing
convention in CLAUDE.md). GitHub's /releases/latest/download/<asset>
URL pattern doesn't resolve to prereleases, so the MCPB download
link in plugin Settings would always 404 without an explicit
promotion gesture.
make promote (no args) flips the current package.json version from
prerelease → stable and marks it Latest. make promote TAG=X.Y.Z
targets a specific older release. Documented inline in CLAUDE.md
next to the existing prerelease convention.
First use: just promoted 0.11.17 manually with the same gh command
this target wraps.
Without this, make release-patch leaves mcpb/manifest.json one
version behind on main after each release — sync-version updates it
on disk via the npm version hook but _commit-and-publish never
stages it. The built .mcpb artifact in CI is correct (build runs
sync-version fresh), but main drifts until the next release
self-heals.
Direct consequence of the earlier "build: Sync mcpb/manifest.json
version with package.json" commit in this PR.
Default view now shows only the two common paths: Claude Desktop
MCPB download and Claude Code CLI string. Everything else moves into
a <details> windowshade ("Advanced — other MCP clients, multi-vault,
custom bundles"), collapsed by default:
- JSON mcpServers block (for Cline, Continue, custom integrations,
and multi-vault setups)
- Pointer to scripts/make-mcpb.mjs for custom-named bundles per vault
Cleaner default surface, advanced paths still one click away. The
maker-script note moved out of the MCPB section header so the
primary path is just "download, paste, install" with no asterisks.
Three drift points caught in review:
- §4 (CI publishing): drop the promised "second workflow publishes
latest .mcpb on mcpb/ change" — it isn't implemented and isn't
needed. Release-time publishing is the actual flow. Also documents
the versioned + unversioned dual-emit and the stable-URL motivation.
- §2 (server.js behavior): the implementation surfaces JSON-RPC
-32000 on session expiry and lets the client reinitialize rather
than attempting transparent replay. ADR previously claimed replay;
text now matches code with the rationale (replay would require
re-issuing state-establishing notifications and that path is
fragile).
- Consequences: acknowledge that the maker script partially exposes
the bridge to advanced users, which slightly weakens the
"bridge-is-invisible" framing of ADR-100/102. The audience that
reads server.js is the same audience that would have written
custom mcpServers JSON anyway, so the abstraction holds for the
cohort that benefits from it.
Refs PR #155 review.
The Settings UI was building a versioned releases/download/<version>/
URL that would 404 for any plugin version not yet associated with a
GitHub release — the exact case for users on a fresh BRAT install
running ahead of the release pipeline.
build-mcpb.mjs now emits both the versioned filename (for archival)
and an unversioned obsidian-mcp.mcpb alias. release.yml uploads both.
The Settings UI links to releases/latest/download/obsidian-mcp.mcpb,
which GitHub resolves to the most recent release's asset by name —
stable regardless of plugin version.
Makefile clean target updated to remove the new artifact.
Refs PR #155 review.
- Serialize the first request: messages arriving while initialize is
in flight now wait on the initialize promise before posting, so
they don't race past the session-id capture. Latent under Claude
Desktop (it serializes), but real defense-in-depth for any client
that doesn't.
- AbortSignal.timeout(30s) on POSTs so an unresponsive server can't
hang the bridge forever. Notify-stream GET stays unbounded — it's
long-lived by design.
- DELETE the session on stdin EOF. MCP spec asks clients to retire
sessions cleanly; previously the bridge just exited and the
server's session lingered until idle GC.
- SSE frame splitter now tolerates CRLF (\r?\n\r?\n) in addition to
LF, matching the spec's "newline" wording more broadly.
- startNotifyStream and the 202-Accepted path now drain (cancel)
non-SSE bodies so the underlying sockets release immediately.
Verifies clean (npm run build + lint + 127 tests pass). Manual
end-to-end install in Claude Desktop continues to work.
Refs PR #155 review.
package.json had a stale "Aaron Blumenfeld" that propagated into
mcpb/manifest.json when sync-version ran. The top-level manifest.json
already had the right value (Aaron Bockelie, matching git author).
Three onboarding paths now documented in priority order: MCPB
one-click install for Claude Desktop (with cross-platform mime-type
caveat), Claude Code CLI, and the JSON config block for other MCP
clients and multi-vault setups. The maker CLI (scripts/make-mcpb.mjs)
gets its own advanced section for custom-named bundles.
Mirrors the Settings UI ordering so the README and the in-plugin
guidance tell the same story.
Refs ADR-102.
Reorders the protocol-information section to surface the easiest
onboarding path first. New section order:
1. Claude desktop (.mcpb — one-click install) ← primary
2. Claude code (CLI) ← unchanged
3. Other mcp clients (JSON config) ← demoted, renamed
The MCPB section shows a versioned download link that resolves to
the GitHub release asset and a values panel (URL + API key with
copy buttons) for the install prompt. A footnote points multi-vault
users at scripts/make-mcpb.mjs for custom-named bundles.
eslint config now ignores mcpb/ and scripts/ — they ship as zip
content and standalone Node CLIs respectively, not plugin code.
Refs ADR-102.
Power users with multiple Obsidian vaults (or anyone wanting a
custom-named bundle) run `node scripts/make-mcpb.mjs`, answer three
prompts (display name, URL, API key), and get an
obsidian-mcp-<slug>.mcpb with the chosen name and defaults
pre-filled. Drop it into Claude Desktop — one-click install with no
manual entry in the user_config dialog.
Shares the zip builder with scripts/build-mcpb.mjs so there's a
single source of truth for the bundle format. Zero deps; Node ≥18.
This is the path ADR-102 §6 designates for multi-vault setups,
replacing both the rejected N-slot manifest design and the
multi-variant publishing alternative.
Refs ADR-102.
scripts/build-mcpb.mjs is a zero-dep pure-Node zip builder
(stored-only container, manual CRC32) that packs mcpb/manifest.json
and mcpb/server.js into obsidian-mcp-<version>.mcpb. Works on any
Node ≥18, doesn't require system zip/7z, runs identically on dev
machines and in CI.
Makefile gains a "mcpb" target so the local workflow is "make mcpb"
when iterating. clean now also removes built .mcpb files.
release.yml builds the bundle as part of the release job and attaches
it to the GitHub release alongside main.js/manifest.json/styles.css,
so BRAT users and the future plugin Settings "download" affordance
both resolve to the canonical release asset.
Refs ADR-102.
Extends sync-version.mjs so a single source of truth (package.json
version) drives manifest.json, mcpb/manifest.json, and version.ts in
lockstep. Future releases bump one file; the MCPB stays current
automatically.
Refs ADR-102.