Skip to content

v4.2.5 feat(install): plugin-marketplace channel + one-line upgrade flow#46

Merged
kevintseng merged 7 commits into
mainfrom
feat/plugin-upgrade-v4.2.5
May 13, 2026
Merged

v4.2.5 feat(install): plugin-marketplace channel + one-line upgrade flow#46
kevintseng merged 7 commits into
mainfrom
feat/plugin-upgrade-v4.2.5

Conversation

@kevintseng
Copy link
Copy Markdown
Contributor

Summary

Claude Code's plugin marketplace pins versions at install time and does not auto-update. Every v4.2.3 user was stuck on the broken npx -y -p @pcircle/memesh memesh-mcp launcher until they manually reinstalled. v4.2.4 fixed the launcher; this PR adds the missing UX so any future stale-version situation surfaces a banner + a one-command fix.

Highlights

  • New plugin-marketplace install channel (src/core/install-channel.ts) detects ~/.claude/plugins/cache/<marketplace>/<plugin>/<version> paths and routes through its own InstallChannelSupport entry. Previously these were classified as unknown and got generic guidance.
  • scripts/upgrade-plugin.sh — idempotent one-line upgrade. Fast-forwards the marketplace cache, rsyncs to a new version directory, runs npm install --omit=dev, patches installed_plugins.json.
  • scripts/hooks/session-start.js — new buildUpdateAvailableBanner(). Fires when the installed version is not deprecated but a newer release exists on npm. Single info line with the channel-tailored upgrade command. 24h throttle per installed version. Defers to the existing deprecation banner when both apply.
  • README "Upgrading" section documents all three upgrade paths (plugin UI, one-line script, npm-global self-update).

Docs synced

  • CHANGELOG.md updated for the change
  • docs/ARCHITECTURE.md updated (version header bumped — module list unchanged)
  • docs/api/API_REFERENCE.md updated (version header bumped — surface unchanged)
  • README.md updated (new "Upgrading" section)
  • README locales (de/vi/th/pt/ja/ko/zh-CN/zh-TW/es/fr) intentionally not modified — adding one English H2 leaves drift at +1, within the locale-parity ±1 tolerance; memesh doctor reports PASS for README locale parity
  • CLAUDE.md updated (gitignored — Project Architecture (v4.2.5) header + install-channel description bumped locally)
  • Version files bumped consistently: package.json + plugin.json + marketplace.json + CHANGELOG header + ARCHITECTURE version line + API_REFERENCE version line (4.2.4 -> 4.2.5)
  • dist/skills-manifest.json regenerated via npm run build
  • memesh doctor reports Overall: PASS_WITH_CONCERNS — only WARN is unrelated hook-activity on a cold install; all version/manifest/locale checks PASS
  • Cumulative code-review reviewer brief includes doc-sync verification (this PR body)

Test plan

  • npm test -- --run passes: 1035 / 1035 (was 1031 baseline; +4 new — install-channel detection + support entry)
  • npm run build passes (smoke test: 6/6)
  • node dist/transports/cli/cli.js doctor → Overall: PASS_WITH_CONCERNS (only WARN is unrelated Hook activity (last 24h) — db has 0 entities locally)
  • npm pack --dry-run confirms scripts/upgrade-plugin.sh ships in the tarball (7.0kB, total 233 files)
  • bash -n scripts/upgrade-plugin.sh — syntax clean
  • Manual dry-run: ran git fetch origin && git merge --ff-only against marketplace cache, confirmed rsync + npm install + JSON patch reproduce the working v4.2.4 install state from a v4.2.3 baseline

….2.5)

Claude Code's plugin marketplace pins versions at install time and does
not auto-update — every v4.2.3 user was stuck until they manually
reinstalled. This adds the missing pieces so memesh tells users a new
version exists and gives them a single command to apply it.

- src/core/install-channel.ts: new `plugin-marketplace` channel that
  matches `~/.claude/plugins/cache/<marketplace>/<plugin>/<version>`
  paths. The check runs before the `.git`/npm-global checks because the
  plugin cache is itself a git clone. Channel routes through its own
  `InstallChannelSupport` entry instead of falling into `unknown` (where
  guidance was generic "upgrade via your install method").

- scripts/upgrade-plugin.sh: idempotent one-line upgrade. Fast-forwards
  the marketplace cache, rsyncs to a new version directory under the
  plugin cache, runs `npm install --omit=dev`, patches
  installed_plugins.json. Pre-flights all required tools and refuses to
  destructively reset a local-committed marketplace cache.

- scripts/hooks/session-start.js: new `buildUpdateAvailableBanner()`.
  Fires only when the installed version is NOT deprecated but a newer
  release exists on npm. One info line with the channel-tailored upgrade
  command. Throttled to once per 24h per installed version. Defers to
  the existing deprecation banner when both apply.

- README, CHANGELOG, ARCHITECTURE, API_REFERENCE: documented the three
  upgrade paths and the new channel. Version bumped 4.2.4 -> 4.2.5
  across all 7 anchor files; dist/skills-manifest.json regenerated.

- tests/install-channel.test.ts: covers the new channel detection
  (including the priority-over-.git case) and the new
  `InstallChannelSupport` entry.

Validation:
- npm test --run: 1035 passed (was 1031 baseline)
- npm run build: clean
- memesh doctor: Overall PASS_WITH_CONCERNS (only WARN is unrelated
  hook-activity on a cold install). All security / version / manifest
  checks PASS.
- npm pack --dry-run: confirms scripts/upgrade-plugin.sh ships in the
  tarball.
@github-actions
Copy link
Copy Markdown

Multi-Model Synthesis

Both Claude and Codex reviews above are independent. The most
valuable signals are the issues only ONE model raised
— those
are the blind spots single-model review misses.

Reviewer responsibility: read both, surface non-overlapping
findings, decide which need fixing before merge.

1. upgrade-plugin.sh: stop interpolating bash vars directly into JS
   string literals in sections 2 + CURRENT_VERSION read. The previous
   form would have broken if a path contained `'` or `\n`; the rest of
   the script (section 5) already used the env-passing pattern, so
   this is now consistent.

2. session-start.js: replace lexicographic version compare with
   componentwise numeric `isStrictlyOlder()`. The previous form would
   have silently suppressed the update banner once memesh hit a
   two-digit patch (`'4.2.10' < '4.2.9'` evaluates true). 8/8 cases
   verified, including prerelease tags.

Also regenerated dist/skills-manifest.json. No version bump — this is
polish on the same v4.2.5 PR before merge.

Validation:
- npm test --run: 1035/1035 pass
- npx tsc --noEmit: clean
- bash -n + dry-run upgrade-plugin.sh: clean
- memesh doctor: Overall: PASS
@github-actions
Copy link
Copy Markdown

Multi-Model Synthesis

Both Claude and Codex reviews above are independent. The most
valuable signals are the issues only ONE model raised
— those
are the blind spots single-model review misses.

Reviewer responsibility: read both, surface non-overlapping
findings, decide which need fixing before merge.

Root cause uncovered by /investigate: Claude Code's `/plugin install`
runs `npm install --ignore-scripts` (security default), which skips
better-sqlite3's `install` script (fetches/builds the native .node
binding) AND skips memesh's own `postinstall-rebuild.mjs` safety net.
Result: better-sqlite3 JS files are present but the native binding is
missing. Every Stop hook then reaches tryRequireBetterSqlite(), gets
null, throws "skip-session-capture: better-sqlite3 unavailable", and
exits 0. No error visible to Claude Code, no entity written. 22 Stop
events fired in the investigation session — zero entities captured.

This is the silent-dropout that has been hitting plugin-marketplace
users on Node ABI versions not covered by better-sqlite3 prebuilts
(Node 24 / ABI v137 currently uncovered) since memesh was published
as a plugin. Memesh's "self-improving memory" loop was effectively off
for every affected user.

Two-layer fix:

1. scripts/hooks/_shared.js — when the binding probe fails,
   tryRequireBetterSqlite() now spawns a detached `npm rebuild
   better-sqlite3` in the package root. The current hook still exits
   silently (caller already silent-skips), but the *next* session
   captures normally. Throttled to one attempt per hour via an
   O_EXCL marker (~/.memesh/last-rebuild-attempt.lock) so a
   crash-loop can't drive a rebuild storm. Gated on !_inTestEnv()
   so CI doesn't spawn npm. Stderr trace surfaces both the fix
   attempt AND the manual rebuild command, so the user has a path
   forward even if the async rebuild fails.

2. src/core/doctor.ts — new check `Native SQLite binding`. Probes
   the binding by instantiating `new Database(':memory:')` (a bare
   `require()` is not sufficient — JS wrapper succeeds even when
   the .node is missing). FAIL surfaces the exact rebuild command.
   Catches the silent-dropout class of bug that previously hid
   behind the existing "Hook activity (last 24h)" WARN, which used
   a 24h grace period that swallowed fresh installs.

   ESM-safe: uses `createRequire(pathToFileURL(...).href)` because
   the doctor module compiles to ESM; bare `require('module')`
   would throw "require is not defined".

3. tests/core/doctor.test.ts — 3 new tests cover:
   - missing binding → FAIL with `npm rebuild` fix message
   - node_modules/better-sqlite3 entirely absent → FAIL with
     `npm install --omit=dev` fix message
   - probe succeeds → PASS

Validation:
- npm test --run: 1038/1038 (was 1035, +3 new)
- npm run build: clean
- memesh doctor: Overall: PASS (new check passes on healthy install)
- Manually verified: 4.2.3 install path reproduces the exact failure
  signature ("Could not locate the bindings file"); 4.2.4 install
  with npm install passes the probe.
@github-actions
Copy link
Copy Markdown

Multi-Model Synthesis

Both Claude and Codex reviews above are independent. The most
valuable signals are the issues only ONE model raised
— those
are the blind spots single-model review misses.

Reviewer responsibility: read both, surface non-overlapping
findings, decide which need fixing before merge.

…w doctor text

User-reported UX bug: the doctor banner on the dashboard showed an
alarmist title ("Heads up — memesh setup needs attention") above a
self-contradicting body ("Install method: Installation method
detection — No action needed"). Three intersecting causes:

1. The banner fired for every `PASS_WITH_CONCERNS` doctor result,
   including WARN checks whose `fix` field was a generic
   "No action needed" placeholder — meaning the user had no path
   forward, just an alarming yellow banner above unactionable text.

2. The banner preferred a generic `doctor.<id>.summary` i18n override
   over the raw doctor `summary`/`fix`. For PASS state that's a
   friendlier label, but for FAIL/WARN it destroyed the actual
   diagnostic detail — a "Native binding missing" FAIL would render
   as the generic PASS-state label "Native binding detected", and
   the actual rebuild command was hidden.

3. The `doctor.install-channel.fix: 'No action needed'` i18n
   override was flat-out wrong — `install-channel` is WARN when
   the install method can't be detected; that's actionable, not
   "no action needed".

Three fixes:

1. dashboard/src/components/DoctorBanner.tsx — new `isActionable()`
   filter: FAIL always counts, WARN counts only when `fix` is
   present AND isn't a "no action needed" placeholder. Banner is
   hidden when zero actionable concerns remain. The CLI / API
   still surfaces every doctor finding; the dashboard banner just
   stops popping for ones the user can't act on.

2. DoctorBanner uses raw `c.summary` / `c.fix` directly. The i18n
   override path is removed — for the banner's purpose (showing
   FAIL/WARN), the raw doctor text is always more diagnostic.

3. Banner tiers severity:
   - FAIL → strong title "memesh setup is incomplete" + Get help
     button (file GitHub issue)
   - WARN-only → softer title "memesh has setup notes" (new
     `doctorBanner.warnTitleSoft` i18n key, translated across all
     11 locales), no GitHub-issue CTA — the in-body fix command
     is the actionable path.

4. dashboard/src/lib/i18n.ts — deleted the misleading
   `doctor.install-channel.summary` and `doctor.install-channel.fix`
   overrides across all 11 locales (English + 10 translations).
   The check's real channel-aware fix field now reaches the user
   verbatim.

Validation:
- Built dashboard + main; smoke tests pass
- Manually verified live: doctor → Overall PASS on v4.2.5 install,
  banner correctly hidden
- 2 pre-existing failures on main (tests/transports/http.test.ts
  recall-empty, tests/tools.test.ts auto-archive) reproduce without
  this change — not caused by this PR
@github-actions
Copy link
Copy Markdown

Multi-Model Synthesis

Both Claude and Codex reviews above are independent. The most
valuable signals are the issues only ONE model raised
— those
are the blind spots single-model review misses.

Reviewer responsibility: read both, surface non-overlapping
findings, decide which need fixing before merge.

…prod seam removal

Three findings from a fresh /review pass on the cumulative v4.2.5 diff:

1. [P2] scripts/hooks/_shared.js — detached `npm rebuild` had no 'error'
   listener. If `npm` is missing from PATH (rare but real on containers /
   stripped-down CI), Node emits an async 'error' event that becomes an
   uncaught exception. The outer synchronous try/catch can't catch it,
   so a hook crash here would turn the silent-dropout bug into a
   broken-hook story. Added `child.on('error', () => {})` to match the
   "self-heal is best-effort" contract — the binding probe already
   leaves a stderr breadcrumb with the manual fix.

2. [P3] scripts/hooks/_shared.js — inline `require('fs').statSync` /
   `require('fs').unlinkSync` reached around the module's top-level
   named imports. Added `statSync, unlinkSync` to the import line so
   the file stays consistent.

3. [P3] src/core/doctor.ts — defaultNativeBindingProbe had a
   `process.env.VITEST === 'true'` test seam. Too permissive: a user
   who happened to have VITEST exported in their shell (e.g. shared
   across projects) would silently see PASS on a broken binding —
   masking the exact failure mode this whole check exists to catch.
   Removed the production seam entirely. Tests inject
   `nativeBindingProbeImpl` explicitly via runDoctor options — updated
   the 2 PASS-path doctor tests to pass `() => ({ ok: true })`. Now
   production code paths always exercise the real probe.

Validation:
- npm test --run: 1036/1038 (same 2 pre-existing failures on main —
  tests/transports/http.test.ts recall-empty, tests/tools.test.ts
  auto-archive — unrelated to this change)
- npx vitest run tests/core/doctor.test.ts: 30/30
- memesh doctor (via HTTP): Overall PASS_WITH_CONCERNS, native-binding PASS
- npm run build: clean (smoke 6/6)
@github-actions
Copy link
Copy Markdown

Multi-Model Synthesis

Both Claude and Codex reviews above are independent. The most
valuable signals are the issues only ONE model raised
— those
are the blind spots single-model review misses.

Reviewer responsibility: read both, surface non-overlapping
findings, decide which need fixing before merge.

Comment thread scripts/hooks/_shared.js Fixed
…sis to source

Eight CodeQL findings on PR #46:

[HIGH security] js/file-system-race at scripts/hooks/_shared.js:424
  Root cause: the self-heal block's stale-marker cleanup was a 3-step
  dance (statSync → unlinkSync → openSync('wx')) where a peer hook
  could insert between any two steps. Worst-case outcome was duplicate
  `npm rebuild` spawns or one peer's fresh lock being stomped by
  another peer's stale-cleanup.

  Fix: drop the stale-cleanup path entirely. Use a single atomic
  O_EXCL create. Once the marker exists, every future hook bails.
  If a rebuild fails, the user clears the marker manually — the
  stderr breadcrumb now logs the exact `rm <markerPath> &&
  npm rebuild better-sqlite3` command. No retry-loop here would
  either re-introduce the race or burn CPU on a broken npm config.

[7 dashboard/dist/index.html findings — not actionable in source]
  Three `js/property-access-on-non-object` errors, three
  `js/automatic-semicolon-insertion` notes, one `js/trivial-conditional`
  warning. All on the single-file minified Preact bundle. The
  patterns are minification + bundler artifacts (Vite runtime helpers
  with defensive null-fallback, constant-folded conditionals,
  semicolon-strip), not real bugs in source.

  Fix: add an advanced-setup CodeQL config that includes source
  paths (src/, scripts/, dashboard/src/, tests/, hooks/) and
  excludes built outputs (dist/, dashboard/dist/, minified bundles).
  This is the GitHub-recommended pattern for repos that ship
  pre-built artifacts. Source for each bundled module is already
  analyzed via the `paths` include — the analysis surface is
  unchanged, only the noise is removed.

Validation:
- npm test --run: 1036/1038 (same 2 pre-existing failures unrelated)
- npm run build: clean
- npx tsc --noEmit: clean
- typecheck + targeted hook/doctor/install-channel tests: 59/59
@github-actions
Copy link
Copy Markdown

Multi-Model Synthesis

Both Claude and Codex reviews above are independent. The most
valuable signals are the issues only ONE model raised
— those
are the blind spots single-model review misses.

Reviewer responsibility: read both, surface non-overlapping
findings, decide which need fixing before merge.

Comment thread scripts/hooks/_shared.js Fixed
Comment thread scripts/hooks/_shared.js Fixed
Leftover from the prior commit's TOCTOU refactor — when I dropped the
stale-marker cleanup branch, the imports and TTL constant became dead.
ESLint flagged 3 warnings at --max-warnings 0 (hard CI gate).

Net behavior: unchanged. The marker is one-shot atomic O_EXCL — TTL
was the variable that gated stale-cleanup, which no longer exists.
@github-actions
Copy link
Copy Markdown

Multi-Model Synthesis

Both Claude and Codex reviews above are independent. The most
valuable signals are the issues only ONE model raised
— those
are the blind spots single-model review misses.

Reviewer responsibility: read both, surface non-overlapping
findings, decide which need fixing before merge.

@kevintseng kevintseng merged commit fda4f90 into main May 13, 2026
15 checks passed
@kevintseng kevintseng deleted the feat/plugin-upgrade-v4.2.5 branch May 13, 2026 05:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants