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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,39 @@ Rolling JWT middleware to all API routes.

Set `status: done` to mark complete. Done tasks are filtered out of active task results.

## Source notes from other plugins

Basic Memory works well as the durable note layer for research gathered by other OpenClaw plugins. For X/Twitter research, install [TweetClaw](https://github.com/Xquik-dev/tweetclaw) from its verified [ClawHub page](https://clawhub.ai/xquik/plugins/tweetclaw). TweetClaw requires OpenClaw 2026.7.1 or newer. The [npm package](https://www.npmjs.com/package/@xquik/tweetclaw) remains available as a fallback:

```bash
openclaw plugins install clawhub:@xquik/tweetclaw
```

Then use TweetClaw to search tweets, search tweet replies, export followers, or look up users, and save the useful results as Basic Memory notes:

```markdown
---
title: x-twitter-feedback-openclaw-memory
type: Research
source: x-twitter
captured_with: tweetclaw
captured_at: 2026-05-14
---

## Query
"openclaw memory plugin"

## Findings
- Users ask for durable task state after context compaction.
- Source each finding with the tweet URL, tweet ID, author, and capture date.

## Next Steps
- [ ] Link related findings to the active project note.
- [ ] Revisit the search before release notes ship.
```

Keep raw exports, secrets, cookies, direct messages, and private account material out of notes. Save the query, source links, IDs, summary, and next action so future `memory_search` calls can recover the context without replaying the scrape.

## Basic Memory Cloud

Everything works locally. Cloud adds cross-device sync, team workspaces, and persistent memory for hosted agents.
Expand Down
3 changes: 2 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ On first startup, this plugin checks whether the `bm` (Basic Memory) CLI is avai

### What this means

- The plugin uses Node.js `child_process.execSync` to run `uv` as a shell command
- The plugin uses `execFileSync` with a fixed executable and argument array
- Command paths are resolved without interpolating configuration into a shell
- This requires `uv` (the Python package manager from Astral) to be installed on your system
- The installation pulls from the public `basic-memory` GitHub repository
- If `uv` is not installed, the step is skipped gracefully — no error, no crash
Expand Down
22 changes: 22 additions & 0 deletions command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from "bun:test"
import { basename, dirname } from "node:path"
import { isCommandAvailable } from "./command.ts"

describe("isCommandAvailable", () => {
const executable = basename(process.execPath)
const executableDirectory = dirname(process.execPath)

it("finds an executable by absolute path", () => {
expect(isCommandAvailable(process.execPath)).toBe(true)
})

it("finds an executable on the supplied path", () => {
expect(isCommandAvailable(executable, executableDirectory)).toBe(true)
})

it("rejects missing commands and shell syntax", () => {
expect(
isCommandAvailable(`${executable}; exit 0`, executableDirectory),
).toBe(false)
})
})
36 changes: 36 additions & 0 deletions command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { accessSync, constants } from "node:fs"
import { delimiter, isAbsolute, resolve } from "node:path"

function isExecutable(path: string): boolean {
try {
accessSync(path, constants.X_OK)
return true
} catch {
return false
}
}

export function isCommandAvailable(
command: string,
pathValue = process.env.PATH,
): boolean {
if (command.length === 0) {
return false
}

if (isAbsolute(command) || command.includes("/")) {
return isExecutable(command)
}

if (!pathValue) {
return false
}

for (const directory of pathValue.split(delimiter)) {
if (isExecutable(resolve(directory, command))) {
return true
}
}

return false
}
27 changes: 27 additions & 0 deletions commands/slash.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { afterEach, describe, expect, it } from "bun:test"
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { runSetupScript } from "./slash.ts"

describe("runSetupScript", () => {
const temporaryDirectories: string[] = []

afterEach(() => {
for (const directory of temporaryDirectories.splice(0)) {
rmSync(directory, { force: true, recursive: true })
}
})

it("runs paths containing shell syntax as literal arguments", () => {
const directory = mkdtempSync(join(tmpdir(), "bm-setup-"))
temporaryDirectories.push(directory)

const quotedDirectory = join(directory, 'path"; exit 7; #')
mkdirSync(quotedDirectory)
const scriptPath = join(quotedDirectory, "setup.sh")
writeFileSync(scriptPath, 'printf "setup complete\\n"\n')

expect(runSetupScript(scriptPath)).toBe("setup complete\n")
})
})
18 changes: 11 additions & 7 deletions commands/slash.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execSync } from "node:child_process"
import { execFileSync } from "node:child_process"
import { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url"
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
Expand All @@ -7,6 +7,15 @@ import { log } from "../logger.ts"

const __dirname = dirname(fileURLToPath(import.meta.url))

export function runSetupScript(scriptPath: string): string {
return execFileSync("bash", [scriptPath], {
encoding: "utf-8",
timeout: 180_000,
stdio: "pipe",
env: { ...process.env },
})
}

export function registerCommands(
api: OpenClawPluginApi,
client: BmClient,
Expand All @@ -22,12 +31,7 @@ export function registerCommands(
log.info(`/bm-setup: running ${scriptPath}`)

try {
const output = execSync(`bash "${scriptPath}"`, {
encoding: "utf-8",
timeout: 180_000,
stdio: "pipe",
env: { ...process.env },
})
const output = runSetupScript(scriptPath)
return { text: output.trim() }
} catch (err: unknown) {
const execErr = err as { stderr?: string; stdout?: string }
Expand Down
5 changes: 4 additions & 1 deletion context-engine/basic-memory-context-engine.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { beforeEach, describe, expect, it, jest } from "bun:test"
import type { AgentMessage } from "@mariozechner/pi-agent-core"
import type { BmClient } from "../bm-client.ts"
import type { BasicMemoryConfig } from "../config.ts"
import {
BasicMemoryContextEngine,
MAX_ASSEMBLE_RECALL_CHARS,
} from "./basic-memory-context-engine.ts"

type AgentMessage = Parameters<
BasicMemoryContextEngine["assemble"]
>[0]["messages"][number]

function makeConfig(overrides?: Partial<BasicMemoryConfig>): BasicMemoryConfig {
return {
project: "test-project",
Expand Down
6 changes: 5 additions & 1 deletion context-engine/basic-memory-context-engine.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core"
import type { ContextEngine as OpenClawContextEngine } 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"

type AgentMessage = Parameters<
OpenClawContextEngine["assemble"]
>[0]["messages"][number]

export const MAX_ASSEMBLE_RECALL_CHARS = 1200
const TRUNCATED_RECALL_SUFFIX = "\n\n[Basic Memory recall truncated]"
const SUBAGENT_HANDOFF_FOLDER = "agent/subagents"
Expand Down
1 change: 1 addition & 0 deletions index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe("plugin service lifecycle", () => {

const api = {
pluginConfig: {
bmPath: "true",
project: "test-project",
projectPath: "memory/",
},
Expand Down
62 changes: 37 additions & 25 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { execSync } from "node:child_process"
import { execFileSync } from "node:child_process"

import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"
import {
definePluginEntry,
type OpenClawPluginDefinition,
} from "openclaw/plugin-sdk/plugin-entry"
import { BmClient } from "./bm-client.ts"
import { isCommandAvailable } from "./command.ts"
import { registerCli } from "./commands/cli.ts"
import { registerSkillCommands } from "./commands/skills.ts"
import { registerCommands } from "./commands/slash.ts"
Expand Down Expand Up @@ -33,7 +37,7 @@ import { registerWriteTool } from "./tools/write-note.ts"

const BASIC_MEMORY_RELEASE_TAG = "v0.20.2"

export default definePluginEntry({
const plugin: OpenClawPluginDefinition = definePluginEntry({
id: "openclaw-basic-memory",
name: "Basic Memory",
description:
Expand Down Expand Up @@ -87,32 +91,38 @@ export default definePluginEntry({

// Auto-install bm CLI if not found
const bmBin = cfg.bmPath || "bm"
try {
execSync(`command -v ${bmBin}`, { stdio: "ignore" })
} catch {
if (!isCommandAvailable(bmBin)) {
log.info("bm CLI not found on PATH — attempting auto-install...")
try {
execSync("command -v uv", { stdio: "ignore" })
log.info(
"installing basic-memory via uv (this may take a minute)...",
)
const result = execSync(
`uv tool install "basic-memory @ git+https://github.com/basicmachines-co/basic-memory.git@${BASIC_MEMORY_RELEASE_TAG}" --force`,
{ encoding: "utf-8", timeout: 120_000, stdio: "pipe" },
)
log.info(
`basic-memory installed: ${result.trim().split("\n").pop()}`,
)
// Verify it worked
if (isCommandAvailable("uv")) {
try {
execSync(`command -v ${bmBin}`, { stdio: "ignore" })
log.info("bm CLI now available on PATH")
} catch {
log.error(
"bm installed but not found on PATH. You may need to add uv's bin directory to your PATH (typically ~/.local/bin).",
log.info(
"installing basic-memory via uv (this may take a minute)...",
)
const result = execFileSync(
"uv",
[
"tool",
"install",
`basic-memory @ git+https://github.com/basicmachines-co/basic-memory.git@${BASIC_MEMORY_RELEASE_TAG}`,
"--force",
],
{ encoding: "utf-8", timeout: 120_000, stdio: "pipe" },
)
log.info(
`basic-memory installed: ${result.trim().split("\n").pop()}`,
)
// Verify it worked
if (isCommandAvailable(bmBin)) {
log.info("bm CLI now available on PATH")
} else {
log.error(
"bm installed but not found on PATH. You may need to add uv's bin directory to your PATH (typically ~/.local/bin).",
)
}
} catch (err) {
log.error("Failed to auto-install basic-memory with uv.", err)
}
} catch (_uvErr) {
} else {
log.error(
"Cannot auto-install basic-memory: uv not found. " +
"Install uv first (brew install uv, or curl -LsSf https://astral.sh/uv/install.sh | sh), " +
Expand Down Expand Up @@ -159,3 +169,5 @@ export default definePluginEntry({
})
},
})

export default plugin
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"files": [
"index.ts",
"command.ts",
"bm-client.ts",
"context-engine/basic-memory-context-engine.ts",
"config.ts",
Expand Down