Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"name": "memesh",
"source": "./",
"description": "MeMesh — Local memory for Claude Code and MCP coding agents. One SQLite file, zero cloud required.",
"version": "4.2.4",
"version": "4.2.5",
"author": {
"name": "PCIRCLE AI"
},
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"author": {
"name": "PCIRCLE AI"
},
"version": "4.2.4",
"version": "4.2.5",
"homepage": "https://pcircle.ai/memesh-llm-memory",
"repository": "https://github.com/PCIRCLE-AI/memesh-llm-memory",
"license": "MIT",
Expand Down
37 changes: 37 additions & 0 deletions .github/codeql/codeql-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: "MeMesh CodeQL config"

# Scope CodeQL analysis to source code. Build outputs (TypeScript `tsc`
# output, Vite/Preact bundled+minified single-file dashboard) are
# generated from the source on every release and would otherwise produce
# noise findings that are not actionable against source — e.g.
# `js/property-access-on-non-object` on Vite's runtime helpers,
# `js/automatic-semicolon-insertion` from minification, and
# `js/trivial-conditional` from constant-folded bundler output. The
# corresponding source is already analyzed via the `paths` include below.
#
# `node_modules/` is excluded by CodeQL's defaults but listed here too
# for clarity (CodeQL re-includes shipped node_modules when the package
# imports them, which would bring third-party JS into the analysis).
#
# This is the recommended setup pattern per GitHub's docs:
# https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning#specifying-directories-to-scan
queries:
- uses: security-and-quality

paths:
- src
- scripts
- dashboard/src
- tests
- hooks

paths-ignore:
- dist
- dashboard/dist
- node_modules
- "**/node_modules"
- .claude
- benchmarks
- docs
- "**/*.min.js"
- "**/*.bundle.js"
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
uses: github/codeql-action/init@ed410739ba306e4ebe5e123421a6bd694e494a2b # v4
with:
languages: ${{ matrix.language }}
queries: security-and-quality
config-file: ./.github/codeql/codeql-config.yml

- name: Autobuild
uses: github/codeql-action/autobuild@ed410739ba306e4ebe5e123421a6bd694e494a2b # v4
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@

All notable changes to MeMesh are documented here.

## [4.2.5] — 2026-05-13

### Added
- **`plugin-marketplace` install channel** (`src/core/install-channel.ts`) — `detectInstallChannel()` now recognises `~/.claude/plugins/cache/<marketplace>/<plugin>/<version>` paths and routes them through their own `InstallChannelSupport` entry with channel-specific guidance. Previously plugin-marketplace installs were classified as `unknown`, so doctor + session-start gave generic "upgrade via your install method" hints with no actionable command. The plugin path takes priority over `.git` / npm-global checks because the plugin cache is itself a git clone.
- **`scripts/upgrade-plugin.sh`** — one-line upgrade for Claude Code plugin installs. Fast-forwards the marketplace cache, rsyncs the new version into `~/.claude/plugins/cache/`, installs runtime deps, patches `installed_plugins.json`. Idempotent (no-op when already current). Bridges Claude Code's version-pinned plugin layout to a single upgrade command — the marketplace itself does not auto-update.
- **Session-start "update available" banner** (`scripts/hooks/session-start.js`) — when the installed version is NOT deprecated but a newer release exists on npm, the session-start hook prints a single info line with the channel-tailored upgrade command (`memesh update` for npm-global, `bash <plugin-root>/scripts/upgrade-plugin.sh` for plugin installs, `git pull && npm install && npm run build` for source checkouts, `npm install @pcircle/memesh@latest` for project-local). Throttled to once per 24h per installed version so the banner doesn't nag.
- **README "Upgrading" section** — documents the three upgrade paths (Claude Code `/plugin` UI, one-line script, npm-global self-update) so users on an old version can find the path that applies to them.
- **Hook self-heal for missing `better-sqlite3` native binding** (`scripts/hooks/_shared.js`) — when `tryRequireBetterSqlite()`'s probe fails because the `.node` binding is absent (Claude Code's `/plugin install` runs `npm install --ignore-scripts` by security default, which skips both `better-sqlite3`'s install script AND memesh's `postinstall-rebuild.mjs` safety net), the hook now spawns a detached `npm rebuild better-sqlite3` in the package root. Throttled to one rebuild attempt per hour via an O_EXCL marker (`~/.memesh/last-rebuild-attempt.lock`) so a crash-loop can't drive a rebuild storm. The current hook still silent-skips, but the *next* session captures normally. Without this fix, plugin-marketplace users on Node ABI versions not covered by better-sqlite3 prebuilts (e.g. Node 24 / ABI v137) saw 100% silent dropout of the auto-capture loop — the DB stayed at 0 entities indefinitely.
- **`memesh doctor` native-binding probe** (`src/core/doctor.ts`) — new check `Native SQLite binding` that probes `better-sqlite3` by actually instantiating `new Database(':memory:')` (a bare `require()` is not sufficient — the JS wrapper succeeds even when the binding is missing). FAIL surfaces the exact `npm rebuild` command. Catches the silent-dropout failure mode that previously hid behind the existing "Hook activity (last 24h)" WARN, which used a grace period that swallowed fresh installs.

### Changed
- **Dashboard `DoctorBanner` filters non-actionable WARNs** (`dashboard/src/components/DoctorBanner.tsx`) — the banner used to fire on every `PASS_WITH_CONCERNS` doctor result, including WARN checks whose `fix` field was a generic "No action needed" placeholder. Result: alarmist title ("Heads up — memesh setup needs attention") above a self-contradicting body ("Installation method detection — No action needed"). The banner now only surfaces a check when status is FAIL, OR when status is WARN AND the doctor attached a non-placeholder `fix`. WARN-only banners get a softer title (`memesh has setup notes`) and drop the "Get help → file a GitHub issue" CTA, since the in-body fix command is the actionable path. FAIL banners keep the strong title + GitHub escalation.
- **Dashboard banner uses raw doctor summary/fix** — earlier it preferred a generic `doctor.<id>.summary` i18n override, which obliterated the actual diagnostic detail for WARN/FAIL states. A "binding missing" FAIL would render as the generic PASS-state label "Native binding detected". Now the banner shows what doctor actually said.
- **Removed the misleading `doctor.install-channel.fix: 'No action needed'` i18n overrides** across all 11 locales (`dashboard/src/lib/i18n.ts`) — these were the proximate cause of the self-contradicting banner copy. The check's real `fix` field (channel-specific upgrade instructions for FAIL/WARN) now reaches the user verbatim.

### Fixed
- **TOCTOU race in `tryRequireBetterSqlite()` self-heal block** (`scripts/hooks/_shared.js`) — the stale-marker cleanup path was `statSync → unlinkSync → openSync('wx')`, a 3-step dance 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. Replaced with a single atomic `O_EXCL` claim — once the marker exists, every future hook bails. If a rebuild fails, the user clears the marker manually (the path is logged in the stderr breadcrumb alongside the manual `npm rebuild` command). Flagged by CodeQL as `js/file-system-race` (HIGH security severity).

### Changed
- **CodeQL analysis scoped to source paths** (`.github/codeql/codeql-config.yml`, `.github/workflows/codeql.yml`) — added an advanced-setup config that includes `src/`, `scripts/`, `dashboard/src/`, `tests/`, `hooks/` and excludes built artifacts (`dist/`, `dashboard/dist/`, minified bundles). Built outputs are regenerated from source on every release and would otherwise produce non-actionable findings (`js/property-access-on-non-object` on Vite runtime helpers, `js/automatic-semicolon-insertion` from minification, `js/trivial-conditional` from constant-folded bundler output). The matching source is already analyzed via the `paths` include.

## [4.2.4] — 2026-05-13

### Added
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,26 @@ Core is framework-agnostic. Same logic runs from terminal, HTTP, or MCP.

---

## Upgrading

Claude Code's plugin marketplace pins versions at install time and does **not** auto-update. To pick up a new release:

**Option A — `/plugin` UI**: uninstall `memesh@pcircle-memesh`, then reinstall. Claude Code fetches the latest marketplace version.

**Option B — one-line script** (no UI clicking, idempotent):

```bash
bash ~/.claude/plugins/cache/pcircle-memesh/memesh/<current-version>/scripts/upgrade-plugin.sh
```

The script fast-forwards the marketplace cache, stages the new version under `~/.claude/plugins/cache/`, installs runtime deps, and re-points `installed_plugins.json`. Restart Claude Code afterwards so the MCP server reconnects.

**npm-global installs** (`npm install -g @pcircle/memesh`) can self-update via `memesh update`. Source checkouts: `git pull && npm install && npm run build`.

Session start surfaces a one-line banner (throttled to once per 24h per version) when a newer release is available, and `memesh doctor` reports the upgrade target with the channel-specific command.

---

## Contributing

```bash
Expand Down
14 changes: 7 additions & 7 deletions dashboard/dist/index.html

Large diffs are not rendered by default.

61 changes: 44 additions & 17 deletions dashboard/src/components/DoctorBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,25 @@ export function DoctorBanner() {
if (!doctor) return null;
if (doctor.status === 'PASS') return null;

const concerns = doctor.checks.filter(c => c.status === 'fail' || c.status === 'warn');
// Only surface concerns that are actually actionable to a user.
// FAIL always counts (broken install — user has to act). WARN counts
// ONLY when doctor attached a `fix` hint AND that hint isn't a
// self-contradicting "no action needed" placeholder. Without this
// filter, "PASS_WITH_CONCERNS" produced banners like
// `Install method: Installation method detection — No action needed`
// — alarmist title + non-actionable body. The CLI / `memesh doctor`
// still reports every WARN; the dashboard just stops popping a
// banner for ones the user can't (or shouldn't) act on.
const isActionable = (c: DoctorCheck) => {
if (c.status === 'fail') return true;
if (c.status !== 'warn') return false;
if (!c.fix) return false;
const fix = c.fix.trim().toLowerCase();
if (!fix) return false;
if (fix === 'no action needed' || fix.startsWith('no action')) return false;
return true;
};
const concerns = doctor.checks.filter(isActionable);
if (concerns.length === 0) return null;

// Signature is stable for the same set of failing checks. Sort
Expand Down Expand Up @@ -115,34 +133,43 @@ export function DoctorBanner() {
×
</button>
<div style={{ fontSize: 13, fontWeight: 600, color: tone, marginBottom: 6 }}>
{isFail ? t('doctorBanner.failTitle') : t('doctorBanner.warnTitle')}
{isFail ? t('doctorBanner.failTitle') : t('doctorBanner.warnTitleSoft')}
</div>
<ul style={{ margin: '6px 0 10px', paddingLeft: 18, fontSize: 12, lineHeight: 1.5, color: 'var(--text-2)' }}>
{concerns.slice(0, 3).map(c => {
// Translate known check IDs, fallback to English summary from doctor
const summaryKey = `doctor.${c.id}.summary`;
const fixKey = `doctor.${c.id}.fix`;
const summary = t(summaryKey) !== summaryKey ? t(summaryKey) : c.summary;
const fix = c.fix && t(fixKey) !== fixKey ? t(fixKey) : c.fix;
// Use the raw doctor summary/fix — they carry the actual
// diagnostic detail (e.g. "Install method detected: unknown"
// + the specific rebuild command). Earlier this layer
// preferred a generic `doctor.<id>.summary` i18n override,
// which destroyed FAIL/WARN context: a "binding missing"
// FAIL appeared as the generic "Native binding detected"
// PASS-state label. The raw text is always more useful
// here because the banner only renders WARN/FAIL.
return (
<li key={c.id}>
<strong>{c.label}:</strong> {summary}
{fix && <> — <em style={{ color: 'var(--text-3)' }}>{fix}</em></>}
<strong>{c.label}:</strong> {c.summary}
{c.fix && <> — <em style={{ color: 'var(--text-3)' }}>{c.fix}</em></>}
</li>
);
})}
{concerns.length > 3 && (
<li style={{ color: 'var(--text-3)' }}>…and {concerns.length - 3} more (run `memesh doctor` for full list)</li>
)}
</ul>
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
<button type="button" class="btn" onClick={getHelp} style={{ fontSize: 12, padding: '4px 12px' }}>
{t('doctorBanner.getHelp')}
</button>
<span style={{ fontSize: 11, color: 'var(--text-3)' }}>
{t('doctorBanner.helpHint')}
</span>
</div>
{/* "Get help" pushes a GitHub issue. Only show for FAIL (broken
install — the user can't fix it themselves). For WARN-only
the fix command is already in the list above, so the GitHub
escalation route would be premature and noisy. */}
{isFail && (
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
<button type="button" class="btn" onClick={getHelp} style={{ fontSize: 12, padding: '4px 12px' }}>
{t('doctorBanner.getHelp')}
</button>
<span style={{ fontSize: 11, color: 'var(--text-3)' }}>
{t('doctorBanner.helpHint')}
</span>
</div>
)}
</div>
);
}
Loading
Loading