Skip to content
Open
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
5 changes: 5 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,17 @@ Available providers for storing and retrieving memories:
Extracts memories via LLM, chunks + embeds extracted content, hybrid BM25 + vector search.
Requires: OPENAI_API_KEY (for memory extraction via gpt-4o-mini + embeddings)

basic-memory Basic Memory - local-first Markdown knowledge graph
Writes sessions as notes via the local "bm" CLI; full-text + semantic search.
Requires: basic-memory CLI ("uv tool install basic-memory")

Usage:
-p supermemory Use Supermemory as the memory provider
-p mem0 Use Mem0 as the memory provider
-p zep Use Zep as the memory provider
-p filesystem Use file-based memory (CLAUDE.md style)
-p rag Use hybrid RAG memory (OpenClaw/QMD style)
-p basic-memory Use Basic Memory (local Markdown knowledge graph)
`)
}

Expand Down
41 changes: 41 additions & 0 deletions src/providers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,44 @@ Example: See `src/providers/zep/prompts.ts`
| `supermemory` | `supermemory` | Raw JSON sessions |
| `mem0` | `mem0ai` | v2 API with graph |
| `zep` | `@getzep/zep-cloud` | Graph-based, custom prompts |
| `filesystem` | OpenAI | MEMORY.md-style: LLM-extracted Markdown + text search |
| `rag` | OpenAI | OpenClaw/QMD-style: chunked + embedded, hybrid BM25 + vector |
| `basic-memory` | `bm` CLI | Local Markdown knowledge graph; hybrid FTS + semantic search |

## Basic Memory Setup

[Basic Memory](https://github.com/basicmachines-co/basic-memory) is a local-first
knowledge graph built from Markdown files. The provider drives the local `bm` CLI
(`bm tool ... --local`, JSON output) — no API key or hosted service is required.

1. **Install the CLI** (Python, via [uv](https://docs.astral.sh/uv/)):

```bash
uv tool install basic-memory
# verify
bm --version
```

If the executable is named differently or not on `PATH`, set `BASIC_MEMORY_CLI`
to the command name or absolute path (e.g. `export BASIC_MEMORY_CLI=basic-memory`).

2. **Run the benchmark** — no key needed:

```bash
bun run src/index.ts run -p basic-memory -b locomo -s 5
```

**How it works**

- **Isolation:** the provider points `BASIC_MEMORY_CONFIG_DIR` / `BASIC_MEMORY_HOME`
at `data/providers/basic-memory`, and creates one throwaway BM **project per
`containerTag`**. It never touches your real `~/.config/basic-memory` config or
existing projects.
- **Ingest:** each session is written as a Markdown note via `bm tool write-note`.
- **Indexing:** `awaitIndexing` polls `bm status` until the project's file/db sync
settles, then runs `bm reindex --embeddings` to build vector embeddings (these lag
the FTS index, and are needed for semantic recall).
- **Search:** `bm tool search-notes --hybrid` combines full-text and semantic search.
- **Clear:** the BM project and its on-disk data are removed.

The first search/reindex downloads the embedding model to a local cache (one-time).
63 changes: 63 additions & 0 deletions src/providers/basic-memory/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, expect, test } from "bun:test"
import { formatSessionNote, parseJsonOutput, projectName } from "./index"
import type { UnifiedSession } from "../../types/unified"

describe("projectName", () => {
test("passes through safe characters", () => {
expect(projectName("q1-run_abc-123")).toBe("q1-run_abc-123")
})

test("replaces unsafe characters with dashes", () => {
expect(projectName("q1/run abc:42")).toBe("q1-run-abc-42")
})
})

describe("parseJsonOutput", () => {
test("parses clean JSON", () => {
expect(parseJsonOutput<{ a: number }>('{"a":1}')).toEqual({ a: 1 })
})

test("strips leading CLI noise before the JSON object", () => {
const noisy = 'Fetching 5 files: 100%\nWarning: no HF_TOKEN\n{"results":[{"title":"x"}]}'
expect(parseJsonOutput<{ results: unknown[] }>(noisy)).toEqual({
results: [{ title: "x" }],
})
})

test("parses a JSON array preceded by noise", () => {
expect(parseJsonOutput<number[]>("progress...\n[1,2,3]")).toEqual([1, 2, 3])
})

test("throws when no JSON is present", () => {
expect(() => parseJsonOutput("no json here")).toThrow()
})
})

describe("formatSessionNote", () => {
const session: UnifiedSession = {
sessionId: "s1",
metadata: { date: "2026-03-01T10:00:00Z", formattedDate: "March 1, 2026" },
messages: [
{ role: "user", speaker: "Caroline", content: "I adopted Biscuit." },
{ role: "assistant", speaker: "Melanie", content: "Nice!" },
],
}

test("includes the formatted date and conversation", () => {
const note = formatSessionNote(session)
expect(note).toContain("**Date:** March 1, 2026")
expect(note).toContain("## Conversation")
expect(note).toContain("**Caroline**: I adopted Biscuit.")
expect(note).toContain("**Melanie**: Nice!")
})

test("falls back to role and ISO date when speaker/formattedDate missing", () => {
const note = formatSessionNote({
sessionId: "s2",
metadata: { date: "2026-03-02T00:00:00Z" },
messages: [{ role: "user", content: "hi" }],
})
expect(note).toContain("**Date:** 2026-03-02T00:00:00Z")
expect(note).toContain("**user**: hi")
})
})
Loading