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
6 changes: 4 additions & 2 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Clone and link locally for plugin development:
git clone https://github.com/basicmachines-co/openclaw-basic-memory.git
cd openclaw-basic-memory
bun install
bun run fetch-skills
openclaw plugins install -l "$PWD"
openclaw plugins enable openclaw-basic-memory --slot memory
openclaw gateway restart
Expand Down Expand Up @@ -37,8 +38,9 @@ Or load directly from a path in your OpenClaw config:

```bash
bun run check-types # Type checking
bun run build # Compile package runtime to dist/
bun run lint # Linting
bun test # Run tests (156 tests)
bun test # Run tests
bun run test:int # Real BM MCP integration tests
```

Expand All @@ -64,7 +66,7 @@ BASIC_MEMORY_REPO=/absolute/path/to/basic-memory bun run test:int
This package is published as `@basicmemory/openclaw-basic-memory`.

```bash
# Verify release readiness (types + tests + npm pack dry run)
# Verify release readiness (types + build + tests + npm pack dry run)
just release-check

# Inspect publish payload
Expand Down
56 changes: 38 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Then install the plugin:

```bash
openclaw plugins install @basicmemory/openclaw-basic-memory
openclaw plugins enable openclaw-basic-memory --slot memory
openclaw gateway restart
```

Expand All @@ -54,7 +55,8 @@ Verify:

```bash
openclaw plugins list
openclaw plugins info openclaw-basic-memory
openclaw plugins inspect openclaw-basic-memory --json
openclaw plugins doctor
```

## Configuration
Expand All @@ -63,8 +65,15 @@ openclaw plugins info openclaw-basic-memory

```json5
{
"openclaw-basic-memory": {
enabled: true
plugins: {
entries: {
"openclaw-basic-memory": {
enabled: true
}
},
slots: {
memory: "openclaw-basic-memory"
}
}
}
```
Expand All @@ -75,16 +84,23 @@ This uses sensible defaults: auto-generated project name, maps to your workspace

```json5
{
"openclaw-basic-memory": {
enabled: true,
config: {
project: "my-agent", // BM project name (default: "openclaw-{hostname}")
projectPath: ".", // Project directory (default: workspace root)
memoryDir: "memory/", // Where task notes live
memoryFile: "MEMORY.md", // Working memory file
autoCapture: true, // Record conversations as daily notes
autoRecall: true, // Inject active tasks + recent activity at session start
debug: false // Verbose logging
plugins: {
entries: {
"openclaw-basic-memory": {
enabled: true,
config: {
project: "my-agent", // BM project name (default: "openclaw-{hostname}")
projectPath: ".", // Project directory (default: workspace root)
memoryDir: "memory/", // Where task notes live
memoryFile: "MEMORY.md", // Working memory file
autoCapture: true, // Record conversations as daily notes
autoRecall: true, // Inject active tasks + recent activity at session start
debug: false // Verbose logging
}
}
},
slots: {
memory: "openclaw-basic-memory"
}
}
}
Expand Down Expand Up @@ -172,19 +188,23 @@ openclaw basic-memory status

## Bundled skills

Six skills ship with the plugin — no installation needed:
Ten skills ship with the plugin — no installation needed:

- **memory-tasks** — structured task tracking that survives context compaction
- **memory-reflect** — periodic consolidation of recent notes into durable memory
- **memory-defrag** — cleanup and reorganization of memory files
- **memory-schema** — schema lifecycle (infer, create, validate, diff)
- **memory-ingest** — import existing material into Basic Memory
- **memory-lifecycle** — manage note/project lifecycle workflows
- **memory-literary-analysis** — analyze texts and reading notes
- **memory-metadata-search** — query notes by frontmatter fields
- **memory-notes** — guidance for writing well-structured notes
- **memory-reflect** — periodic consolidation of recent notes into durable memory
- **memory-research** — research synthesis into durable notes
- **memory-schema** — schema lifecycle (infer, create, validate, diff)
- **memory-tasks** — structured task tracking that survives context compaction

### Updating skills

```bash
npx skills add basicmachines-co/basic-memory-skills --agent openclaw
bun run fetch-skills
```

## Task notes
Expand Down
2 changes: 1 addition & 1 deletion commands/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
import type { BmClient } from "../bm-client.ts"
import type { BasicMemoryConfig } from "../config.ts"
import { log } from "../logger.ts"
Expand Down
2 changes: 1 addition & 1 deletion commands/skills.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it, jest } from "bun:test"
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
import { registerSkillCommands } from "./skills.ts"

describe("skill slash commands", () => {
Expand Down
32 changes: 22 additions & 10 deletions commands/skills.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import { readFileSync } from "node:fs"
import { existsSync, readFileSync } from "node:fs"
import { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url"
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"

const __dirname = dirname(fileURLToPath(import.meta.url))
const SKILLS_DIR = resolve(__dirname, "..", "skills")
const MANIFEST_PATH = resolve(SKILLS_DIR, "manifest.json")

interface ManifestEntry {
dir: string
name: string
description: string
}

function loadManifest(): ManifestEntry[] {
function resolveSkillsDir(api: OpenClawPluginApi): string {
if (api.resolvePath) {
return api.resolvePath("skills")
}

const sourceRootSkills = resolve(__dirname, "..", "skills")
if (existsSync(sourceRootSkills)) {
return sourceRootSkills
}

return resolve(__dirname, "..", "..", "skills")
}

function loadManifest(skillsDir: string): ManifestEntry[] {
try {
const raw = readFileSync(MANIFEST_PATH, "utf-8")
const raw = readFileSync(resolve(skillsDir, "manifest.json"), "utf-8")
return JSON.parse(raw) as ManifestEntry[]
} catch {
throw new Error(
Expand All @@ -24,16 +35,17 @@ function loadManifest(): ManifestEntry[] {
}
}

function loadSkill(dir: string): string {
return readFileSync(resolve(SKILLS_DIR, dir, "SKILL.md"), "utf-8")
function loadSkill(skillsDir: string, dir: string): string {
return readFileSync(resolve(skillsDir, dir, "SKILL.md"), "utf-8")
}

export function registerSkillCommands(api: OpenClawPluginApi): void {
const manifest = loadManifest()
const skillsDir = resolveSkillsDir(api)
const manifest = loadManifest(skillsDir)

for (const entry of manifest) {
const commandName = entry.dir.replace(/^memory-/, "")
const content = loadSkill(entry.dir)
const content = loadSkill(skillsDir, entry.dir)

api.registerCommand({
name: commandName,
Expand Down
6 changes: 4 additions & 2 deletions commands/slash.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { execSync } from "node:child_process"
import { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url"
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
import type { BmClient } from "../bm-client.ts"
import { log } from "../logger.ts"

Expand All @@ -16,7 +16,9 @@ export function registerCommands(
description: "Install or update the Basic Memory CLI (requires uv)",
requireAuth: true,
handler: async () => {
const scriptPath = resolve(__dirname, "..", "scripts", "setup-bm.sh")
const scriptPath = api.resolvePath
? api.resolvePath("scripts/setup-bm.sh")
: resolve(__dirname, "..", "scripts", "setup-bm.sh")
log.info(`/bm-setup: running ${scriptPath}`)

try {
Expand Down
12 changes: 12 additions & 0 deletions config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,18 @@ describe("config", () => {
expect(parseConfig({ cloud: null }).cloud).toBeUndefined()
})

it("should throw error for unknown cloud config keys", () => {
expect(() =>
parseConfig({
cloud: {
url: "https://cloud.basicmemory.com",
api_key: "test-key",
extra: true,
},
}),
).toThrow("basic-memory cloud config has unknown keys: extra")
})

it("should throw error for unknown config keys", () => {
expect(() => parseConfig({ unknownKey: "value" })).toThrow(
"basic-memory config has unknown keys: unknownKey",
Expand Down
1 change: 1 addition & 0 deletions config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export function parseConfig(raw: unknown): BasicMemoryConfig {
let cloud: CloudConfig | undefined
if (cfg.cloud && typeof cfg.cloud === "object" && !Array.isArray(cfg.cloud)) {
const c = cfg.cloud as Record<string, unknown>
assertAllowedKeys(c, ["url", "api_key"], "basic-memory cloud config")
if (typeof c.url === "string" && typeof c.api_key === "string") {
cloud = { url: c.url, api_key: c.api_key }
}
Expand Down
95 changes: 38 additions & 57 deletions context-engine/basic-memory-context-engine.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,52 @@
import { createRequire } from "node:module"
import { dirname, resolve } from "node:path"
import { pathToFileURL } from "node:url"
import type { AgentMessage } from "@mariozechner/pi-agent-core"
import type {
AssembleResult,
BootstrapResult,
CompactResult,
ContextEngine,
} from "openclaw/plugin-sdk"
import { delegateCompactionToRuntime } from "openclaw/plugin-sdk/core"
import type { BmClient } from "../bm-client.ts"
import type { BasicMemoryConfig } from "../config.ts"
import { selectCaptureTurn } from "../hooks/capture.ts"
import { loadRecallState } from "../hooks/recall.ts"
import { log } from "../logger.ts"

const require = createRequire(import.meta.url)
export const MAX_ASSEMBLE_RECALL_CHARS = 1200
const TRUNCATED_RECALL_SUFFIX = "\n\n[Basic Memory recall truncated]"
const SUBAGENT_HANDOFF_FOLDER = "agent/subagents"
const MAX_SUBAGENT_RECALL_CHARS = 800

type AssembleResult = {
messages: AgentMessage[]
estimatedTokens: number
systemPromptAddition?: string
}

type BootstrapResult = {
bootstrapped: boolean
importedMessages?: number
reason?: string
}

type CompactResult = {
ok: boolean
compacted: boolean
reason?: string
result?: {
summary?: string
firstKeptEntryId?: string
tokensBefore: number
tokensAfter?: number
details?: unknown
sessionId?: string
sessionFile?: string
}
}

interface ContextEngine {
readonly info: {
id: string
name: string
version?: string
ownsCompaction?: boolean
}
}

interface SessionMemoryState {
recallContext: string
}
Expand Down Expand Up @@ -104,36 +131,6 @@ function buildSubagentCompletionUpdate(params: {
].join("\n")
}

type LegacyContextEngineModule = {
LegacyContextEngine: new () => {
compact(params: {
sessionId: string
sessionFile: string
tokenBudget?: number
force?: boolean
currentTokenCount?: number
compactionTarget?: "budget" | "threshold"
customInstructions?: string
runtimeContext?: Record<string, unknown>
}): Promise<CompactResult>
}
}

async function loadLegacyContextEngine(): Promise<
LegacyContextEngineModule["LegacyContextEngine"]
> {
const pluginSdkPath = require.resolve("openclaw/plugin-sdk")
const legacyPath = resolve(
dirname(pluginSdkPath),
"context-engine",
"legacy.js",
)
const module = (await import(
pathToFileURL(legacyPath).href
)) as LegacyContextEngineModule
return module.LegacyContextEngine
}

export class BasicMemoryContextEngine implements ContextEngine {
readonly info = {
id: "openclaw-basic-memory",
Expand All @@ -144,9 +141,6 @@ export class BasicMemoryContextEngine implements ContextEngine {

private readonly sessionState = new Map<string, SessionMemoryState>()
private readonly subagentState = new Map<string, SubagentHandoffState>()
private legacyContextEnginePromise: Promise<
InstanceType<LegacyContextEngineModule["LegacyContextEngine"]>
> | null = null

constructor(
private readonly client: BmClient,
Expand Down Expand Up @@ -243,8 +237,7 @@ export class BasicMemoryContextEngine implements ContextEngine {
customInstructions?: string
runtimeContext?: Record<string, unknown>
}): Promise<CompactResult> {
const legacy = await this.getLegacyContextEngine()
return legacy.compact(params)
return delegateCompactionToRuntime(params)
}

async prepareSubagentSpawn(params: {
Expand Down Expand Up @@ -315,16 +308,4 @@ export class BasicMemoryContextEngine implements ContextEngine {
this.sessionState.clear()
this.subagentState.clear()
}

private async getLegacyContextEngine(): Promise<
InstanceType<LegacyContextEngineModule["LegacyContextEngine"]>
> {
if (!this.legacyContextEnginePromise) {
this.legacyContextEnginePromise = loadLegacyContextEngine().then(
(LegacyContextEngine) => new LegacyContextEngine(),
)
}

return this.legacyContextEnginePromise
}
}
Loading
Loading