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
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,34 @@ jobs:

- name: Verify packaged artifact
run: npm run test:packaged

dashboard-e2e-smoke:
name: Packaged Dashboard E2E Smoke
needs: build-and-test
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Install Playwright Chromium
run: npx playwright install --with-deps chromium

- name: Build
run: npm run build

- name: Build dashboard
run: cd dashboard && npm ci && npx vite build

- name: Run packaged dashboard e2e smoke
run: npm run test:e2e-dashboard
6 changes: 6 additions & 0 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ jobs:
- name: Smoke test packaged artifact
run: npm run test:packaged

- name: Install Playwright Chromium
run: npx playwright install --with-deps chromium

- name: Smoke test packaged dashboard
run: npm run test:e2e-dashboard

- name: Verify package contents
run: npm pack --dry-run

Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG-4.0.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This release addresses dashboard access, security, data integrity, release-readi
- Vector persistence, vector isolation, hook state isolation, and session tracking are corrected
- Clean npm consumer installs no longer inherit the stale Xenova ONNX dependency chain

Published v4.0.1 had 445 tests. The current v4.0.2 release candidate has 452 tests. No breaking changes.
Published v4.0.1 had 445 tests. The current v4.0.2 release candidate has 463 tests plus post-publication security hardening around import trust, hook injection boundaries, remote HTTP bind safety, and local file permissions. No breaking changes.

---

Expand Down
16 changes: 12 additions & 4 deletions CHANGELOG-4.0.2.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MeMesh v4.0.2 — Release-Readiness Fixes

**Release Date:** 2026-04-23
**Release Date:** 2026-04-24
**Type:** Patch release candidate after npm v4.0.1 publication

---
Expand All @@ -18,21 +18,29 @@ v4.0.1 is already published on npm, so the follow-up reliability, install, and b
- **Clean consumer install audit:** replaced stale `@xenova/transformers` with maintained `@huggingface/transformers`, removing the vulnerable `onnxruntime-web -> onnx-proto -> protobufjs@6` chain from clean installs.
- **Capability reporting:** Level 0/no-LLM mode reports `onnx` when the local Transformers.js provider is available.
- **Dashboard browser smoke:** `/favicon.ico` returns 204 so packaged dashboard browser smoke is console-clean.
- **Packaged dashboard e2e smoke:** added `npm run test:e2e-dashboard`, which packs the tarball, serves the packaged dashboard, verifies Browse/Search/Settings, confirms locale switches in-place, and fails on page/console errors.
- **Dashboard i18n UX:** all 11 locales have translation key parity, and language changes apply immediately without a full-page reload.
- **Imported memory trust boundary:** imported entities are now marked `trust: untrusted` with import provenance, so shared bundles remain searchable but are excluded from automatic Claude hook injection until reviewed.
- **Hook prompt-injection hardening:** session-start and pre-edit hooks wrap recalled memories as reference data and skip untrusted/imported entities during automatic injection.
- **HTTP bind safety:** `memesh serve` refuses non-loopback hosts unless `--allow-remote` or `MEMESH_HTTP_ALLOW_REMOTE=true` is explicitly set.
- **Private local artifacts:** config, hook throttle state, and session recall-tracking files are chmod-hardened after write (`0700` dirs, `0600` files).

## Documentation

- README test count updated to 452 tests.
- README test count updated to 463 tests.
- `CHANGELOG.md` now distinguishes published v4.0.1 from v4.0.2 follow-up fixes.
- `docs/plans/README.md` marks historical plans as archived context, not active backlog.
- Obsidian project notes were updated outside the repo to mark stale package facts as historical and note that v4.0.2 follow-up fixes are not published until a new npm release is cut.
- API reference now documents imported-memory auto-context behavior and remote HTTP bind opt-in.

## Verification

- `npm test` — 29 files, 452 tests passed.
- `npm test` — 30 files, 463 tests passed.
- `npm run typecheck` — passed.
- `npm run build` — passed.
- `npm audit --omit=dev --json` — 0 vulnerabilities.
- `npm run test:packaged` — passed.
- Clean-machine packed install smoke — fresh temp app install, CLI `remember`, CLI `recall`, and clean consumer `npm audit --omit=dev` passed.
- Packaged dashboard browser smoke — `/dashboard` rendered from the packed install with 0 Playwright console warnings/errors.
- Packaged dashboard browser smoke — `/dashboard` rendered from the packed install with 0 Playwright console warnings/errors; English -> Traditional Chinese -> English language switching updated in-place.
- Packaged dashboard e2e smoke — passed via `npm run test:e2e-dashboard`.
- npm registry verification — npm latest is still v4.0.1 at published gitHead `c936c2548ff886b884c4ba40c83a080b467b4e17`; v4.0.2 is not published yet.
12 changes: 9 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

All notable changes to MeMesh are documented here.

## [4.0.2] — 2026-04-23
## [4.0.2] — 2026-04-24

### Fixed
- **sqlite-vec Vector Persistence** — Fixed vector writes by binding vec0 row IDs as `BigInt`, replacing vectors via delete+insert, and using byte-offset-safe embedding blobs. CLI `remember` now flushes queued embeddings before closing the database.
Expand All @@ -11,11 +11,17 @@ All notable changes to MeMesh are documented here.
- **Clean Consumer Install Audit** — Replaced stale `@xenova/transformers` with maintained `@huggingface/transformers`, removing the vulnerable `onnxruntime-web -> onnx-proto -> protobufjs@6` dependency chain for clean npm consumers.
- **Embedding Capability Reporting** — Level 0/no-LLM mode now reports `onnx` when the local Transformers.js provider is available, matching the actual runtime embedding fallback.
- **Dashboard Browser Smoke** — Added a no-content favicon response so packaged dashboard browser smoke tests stay console-clean.
- **Packaged Dashboard E2E Smoke** — Added a Playwright-based `npm run test:e2e-dashboard` flow that packs the tarball, serves the packaged dashboard, verifies Browse/Search/Settings, checks instant locale switching without reload, and fails on page/console errors.
- **Dashboard i18n UX** — All 11 locales now have translation key parity, and language changes apply immediately without a full-page reload.
- **Imported Memory Trust Boundary** — Imported memories are now marked `trust: untrusted` with import provenance, so team/shared bundles stay searchable but are excluded from automatic Claude hook injection until reviewed.
- **Hook Context Guardrails** — Session-start and pre-edit hooks now wrap recalled memories as reference data rather than raw instructions, and they skip untrusted/imported entities during automatic injection.
- **HTTP Remote Bind Guard** — `memesh serve` now refuses non-loopback hosts unless you pass `--allow-remote` or set `MEMESH_HTTP_ALLOW_REMOTE=true`, preventing accidental unauthenticated LAN exposure.
- **Private Local Artifact Permissions** — Config, hook throttle state, and session recall-tracking files are now chmod-hardened after write (`0700` dirs, `0600` files) instead of relying on creation mode alone.

### Changed
- Added `docs/plans/README.md` to mark historical plans as archived context, not active backlog.
- 452 tests passing across 29 test files.
- Verified clean-machine packed install, clean consumer audit, packaged CLI smoke, packaged dashboard browser smoke, and npm registry publication status.
- 463 tests passing across 30 test files.
- Verified clean-machine packed install, clean consumer audit, packaged CLI smoke, packaged dashboard browser/i18n smoke, packaged dashboard e2e smoke, and npm registry publication status.

## [4.0.1] — 2026-04-21

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ You don't need to manually remember everything. MeMesh has **5 hooks** that capt
| **Graph** | Interactive force-directed knowledge graph with type filters, search, ego mode, recency heatmap |
| **Lessons** | Structured lessons from past failures (error, root cause, fix, prevention) |
| **Manage** | Archive and restore entities |
| **Settings** | LLM provider config, language selector |
| **Settings** | LLM provider config, instant language selector |

---

Expand All @@ -178,6 +178,7 @@ You don't need to manually remember everything. MeMesh has **5 hooks** that capt
**⚠️ Conflict Detection** — If you have two memories that contradict each other, MeMesh warns you.

**📦 Team Sharing** — `memesh export > team-knowledge.json` → share with your team → `memesh import team-knowledge.json`
Imported bundles stay searchable, but MeMesh does not auto-inject imported memories into Claude hooks until you review or re-store them locally.

---

Expand Down Expand Up @@ -259,7 +260,8 @@ Core is framework-agnostic. Same logic runs from terminal, HTTP, or MCP.
```bash
git clone https://github.com/PCIRCLE-AI/memesh-llm-memory
cd memesh-llm-memory && npm install && npm run build
npm test # 452 tests
npm test # 463 tests
npm run test:e2e-dashboard
```

Dashboard: `cd dashboard && npm install && npm run dev`
Expand Down
9 changes: 5 additions & 4 deletions dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import { GraphTab } from './components/GraphTab';
import { LessonsTab } from './components/LessonsTab';
import { FeedbackWidget } from './components/FeedbackWidget';
import { api, type HealthData } from './lib/api';
import { initLocale, t } from './lib/i18n';

initLocale();
import { initLocale, t, type Locale } from './lib/i18n';

const TAB_KEYS = ['Search', 'Browse', 'Analytics', 'Graph', 'Lessons', 'Manage', 'Settings'] as const;
type Tab = typeof TAB_KEYS[number];
Expand All @@ -27,6 +25,7 @@ const TAB_I18N_KEYS: Record<Tab, string> = {
};

export function App() {
const [locale, setLocale] = useState<Locale>(() => initLocale());
const [tab, setTab] = useState<Tab>('Browse');
const [health, setHealth] = useState<HealthData | null>(null);
const [error, setError] = useState('');
Expand All @@ -51,7 +50,9 @@ export function App() {
<div class={`panel ${tab === 'Graph' ? 'active' : ''}`}>{tab === 'Graph' && <GraphTab />}</div>
<div class={`panel ${tab === 'Lessons' ? 'active' : ''}`}>{tab === 'Lessons' && <LessonsTab />}</div>
<div class={`panel ${tab === 'Manage' ? 'active' : ''}`}>{tab === 'Manage' && <BrowseTab manage />}</div>
<div class={`panel ${tab === 'Settings' ? 'active' : ''}`}>{tab === 'Settings' && <SettingsTab />}</div>
<div class={`panel ${tab === 'Settings' ? 'active' : ''}`}>
{tab === 'Settings' && <SettingsTab locale={locale} onLocaleChange={setLocale} />}
</div>
</div>
<FeedbackWidget health={health} />
</div>
Expand Down
96 changes: 55 additions & 41 deletions dashboard/src/components/SettingsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { useState, useEffect } from 'preact/hooks';
import { api, type ConfigData } from '../lib/api';
import { t, getLocale, setLocale, getLocales, type Locale } from '../lib/i18n';
import { t, setLocale, getLocales, type Locale } from '../lib/i18n';

interface SettingsTabProps {
locale: Locale;
onLocaleChange: (locale: Locale) => void;
}

function capitalize(s: string): string {
if (!s) return s;
return s.charAt(0).toUpperCase() + s.slice(1);
}

export function SettingsTab() {
export function SettingsTab({ locale, onLocaleChange }: SettingsTabProps) {
const [config, setConfig] = useState<ConfigData | null>(null);
const [provider, setProvider] = useState('');
const [apiKey, setApiKey] = useState('');
Expand Down Expand Up @@ -71,59 +76,68 @@ export function SettingsTab() {
{/* LLM Config */}
<div class="card">
<div class="card-title">{t('settings.llmProvider')}</div>
<div style={{ display: 'flex', gap: 16, marginBottom: 14 }}>
{([['anthropic', 'Anthropic (Claude)'], ['openai', 'OpenAI'], ['ollama', 'Ollama (Local)']] as const).map(([val, label]) => (
<label key={val} style={{ display: 'flex', alignItems: 'center', gap: 5, cursor: 'pointer', fontSize: 13 }}>
<form
onSubmit={(e) => {
e.preventDefault();
void save();
}}
>
<div style={{ display: 'flex', gap: 16, marginBottom: 14 }}>
{([['anthropic', 'Anthropic (Claude)'], ['openai', 'OpenAI'], ['ollama', 'Ollama (Local)']] as const).map(([val, label]) => (
<label key={val} style={{ display: 'flex', alignItems: 'center', gap: 5, cursor: 'pointer', fontSize: 13 }}>
<input
type="radio"
name="provider"
value={val}
checked={provider === val}
onChange={() => setProvider(val)}
/>
{label}
</label>
))}
</div>

{provider && provider !== 'ollama' && (
<div style={{ marginBottom: 12 }}>
<label style={{ fontSize: 12, color: 'var(--text-2)', display: 'block', marginBottom: 4 }}>{t('settings.apiKey')}</label>
<input
type="radio"
name="provider"
value={val}
checked={provider === val}
onChange={() => setProvider(val)}
type="password"
autoComplete="off"
placeholder={provider === 'anthropic' ? 'sk-ant-api03-…' : 'sk-…'}
value={apiKey}
onInput={(e) => setApiKey((e.target as HTMLInputElement).value)}
/>
{label}
</label>
))}
</div>
</div>
)}

{provider && provider !== 'ollama' && (
<div style={{ marginBottom: 12 }}>
<label style={{ fontSize: 12, color: 'var(--text-2)', display: 'block', marginBottom: 4 }}>{t('settings.apiKey')}</label>
<div style={{ marginBottom: 14 }}>
<label style={{ fontSize: 12, color: 'var(--text-2)', display: 'block', marginBottom: 4 }}>{t('settings.model')}</label>
<input
type="password"
placeholder={provider === 'anthropic' ? 'sk-ant-api03-…' : 'sk-…'}
value={apiKey}
onInput={(e) => setApiKey((e.target as HTMLInputElement).value)}
type="text"
placeholder={provider === 'anthropic' ? 'claude-haiku-4-5' : provider === 'openai' ? 'gpt-4o-mini' : 'llama3.2'}
value={model}
onInput={(e) => setModel((e.target as HTMLInputElement).value)}
/>
</div>
)}

<div style={{ marginBottom: 14 }}>
<label style={{ fontSize: 12, color: 'var(--text-2)', display: 'block', marginBottom: 4 }}>{t('settings.model')}</label>
<input
type="text"
placeholder={provider === 'anthropic' ? 'claude-haiku-4-5' : provider === 'openai' ? 'gpt-4o-mini' : 'llama3.2'}
value={model}
onInput={(e) => setModel((e.target as HTMLInputElement).value)}
/>
</div>

<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
<button class="btn btn-primary" onClick={save} disabled={!provider || saving}>
{saving ? t('settings.saving') : t('settings.save')}
</button>
{msg && <span style={{ fontSize: 12, color: msg.startsWith(t('common.error')) ? 'var(--danger)' : 'var(--success)' }}>{msg}</span>}
</div>
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
<button class="btn btn-primary" type="submit" disabled={!provider || saving}>
{saving ? t('settings.saving') : t('settings.save')}
</button>
{msg && <span style={{ fontSize: 12, color: msg.startsWith(t('common.error')) ? 'var(--danger)' : 'var(--success)' }}>{msg}</span>}
</div>
</form>
</div>

{/* Language */}
<div class="card">
<div class="card-title">{t('settings.language')}</div>
<select
value={getLocale()}
value={locale}
onChange={(e) => {
setLocale((e.target as HTMLSelectElement).value as Locale);
window.location.reload();
const nextLocale = (e.target as HTMLSelectElement).value as Locale;
setLocale(nextLocale);
onLocaleChange(nextLocale);
}}
style={{ fontSize: 13, padding: '6px 10px', borderRadius: 6, border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text-1)', cursor: 'pointer' }}
>
Expand Down
Loading
Loading