@basicmemory/openclaw-basic-memory 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -152,6 +152,7 @@ All tools accept an optional `project` parameter for cross-project operations.
152
152
 
153
153
  | Command | Description |
154
154
  |---------|-------------|
155
+ | `/bm-setup` | Install or update the Basic Memory CLI |
155
156
  | `/remember <text>` | Save a quick note |
156
157
  | `/recall <query>` | Search the knowledge graph |
157
158
  | `/tasks [args]` | Create, track, resume structured tasks |
package/bm-client.ts CHANGED
@@ -5,6 +5,15 @@ import { log } from "./logger.ts"
5
5
 
6
6
  const DEFAULT_RETRY_DELAYS_MS = [500, 1000, 2000]
7
7
 
8
+ export class NoteAlreadyExistsError extends Error {
9
+ readonly permalink: string
10
+ constructor(title: string, permalink: string) {
11
+ super(`Note already exists: "${title}" (${permalink})`)
12
+ this.name = "NoteAlreadyExistsError"
13
+ this.permalink = permalink
14
+ }
15
+ }
16
+
8
17
  const REQUIRED_TOOLS = [
9
18
  "search_notes",
10
19
  "read_note",
@@ -569,6 +578,7 @@ export class BmClient {
569
578
  content: string,
570
579
  folder: string,
571
580
  project?: string,
581
+ overwrite?: boolean,
572
582
  ): Promise<NoteResult> {
573
583
  const args: Record<string, unknown> = {
574
584
  title,
@@ -577,6 +587,7 @@ export class BmClient {
577
587
  output_format: "json",
578
588
  }
579
589
  if (project) args.project = project
590
+ if (overwrite !== undefined) args.overwrite = overwrite
580
591
 
581
592
  const payload = await this.callTool("write_note", args)
582
593
 
@@ -584,6 +595,13 @@ export class BmClient {
584
595
  throw new Error("invalid write_note response")
585
596
  }
586
597
 
598
+ if (payload.error === "NOTE_ALREADY_EXISTS") {
599
+ throw new NoteAlreadyExistsError(
600
+ asString(payload.title) ?? title,
601
+ asString(payload.permalink) ?? "",
602
+ )
603
+ }
604
+
587
605
  const resultTitle = asString(payload.title)
588
606
  const permalink = asString(payload.permalink)
589
607
  const filePath = asString(payload.file_path)
@@ -850,7 +868,7 @@ export class BmClient {
850
868
  ].join("\n")
851
869
 
852
870
  try {
853
- await this.writeNote(title, content, "conversations")
871
+ await this.writeNote(title, content, "conversations", undefined, true)
854
872
  log.debug(`created conversation note: ${title}`)
855
873
  } catch (err) {
856
874
  log.error("conversation index failed", err)
@@ -5,39 +5,39 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
5
5
 
6
6
  const __dirname = dirname(fileURLToPath(import.meta.url))
7
7
  const SKILLS_DIR = resolve(__dirname, "..", "skills")
8
+ const MANIFEST_PATH = resolve(SKILLS_DIR, "manifest.json")
9
+
10
+ interface ManifestEntry {
11
+ dir: string
12
+ name: string
13
+ description: string
14
+ }
15
+
16
+ function loadManifest(): ManifestEntry[] {
17
+ try {
18
+ const raw = readFileSync(MANIFEST_PATH, "utf-8")
19
+ return JSON.parse(raw) as ManifestEntry[]
20
+ } catch {
21
+ throw new Error(
22
+ "skills/manifest.json not found. Run `bun scripts/fetch-skills.ts` first.",
23
+ )
24
+ }
25
+ }
8
26
 
9
27
  function loadSkill(dir: string): string {
10
28
  return readFileSync(resolve(SKILLS_DIR, dir, "SKILL.md"), "utf-8")
11
29
  }
12
30
 
13
- const SKILLS = [
14
- { name: "tasks", dir: "memory-tasks", desc: "Task management workflow" },
15
- {
16
- name: "reflect",
17
- dir: "memory-reflect",
18
- desc: "Memory reflection workflow",
19
- },
20
- { name: "defrag", dir: "memory-defrag", desc: "Memory defrag workflow" },
21
- { name: "schema", dir: "memory-schema", desc: "Schema management workflow" },
22
- {
23
- name: "notes",
24
- dir: "memory-notes",
25
- desc: "How to write well-structured notes",
26
- },
27
- {
28
- name: "metadata-search",
29
- dir: "memory-metadata-search",
30
- desc: "Structured metadata search workflow",
31
- },
32
- ] as const
33
-
34
31
  export function registerSkillCommands(api: OpenClawPluginApi): void {
35
- for (const skill of SKILLS) {
36
- const content = loadSkill(skill.dir)
32
+ const manifest = loadManifest()
33
+
34
+ for (const entry of manifest) {
35
+ const commandName = entry.dir.replace(/^memory-/, "")
36
+ const content = loadSkill(entry.dir)
37
37
 
38
38
  api.registerCommand({
39
- name: skill.name,
40
- description: skill.desc,
39
+ name: commandName,
40
+ description: entry.description,
41
41
  acceptsArgs: true,
42
42
  requireAuth: true,
43
43
  handler: async (ctx: { args?: string }) => {
package/commands/slash.ts CHANGED
@@ -1,11 +1,43 @@
1
+ import { execSync } from "node:child_process"
2
+ import { dirname, resolve } from "node:path"
3
+ import { fileURLToPath } from "node:url"
1
4
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
2
5
  import type { BmClient } from "../bm-client.ts"
3
6
  import { log } from "../logger.ts"
4
7
 
8
+ const __dirname = dirname(fileURLToPath(import.meta.url))
9
+
5
10
  export function registerCommands(
6
11
  api: OpenClawPluginApi,
7
12
  client: BmClient,
8
13
  ): void {
14
+ api.registerCommand({
15
+ name: "bm-setup",
16
+ description: "Install or update the Basic Memory CLI (requires uv)",
17
+ requireAuth: true,
18
+ handler: async () => {
19
+ const scriptPath = resolve(__dirname, "..", "scripts", "setup-bm.sh")
20
+ log.info(`/bm-setup: running ${scriptPath}`)
21
+
22
+ try {
23
+ const output = execSync(`bash "${scriptPath}"`, {
24
+ encoding: "utf-8",
25
+ timeout: 180_000,
26
+ stdio: "pipe",
27
+ env: { ...process.env },
28
+ })
29
+ return { text: output.trim() }
30
+ } catch (err: unknown) {
31
+ const execErr = err as { stderr?: string; stdout?: string }
32
+ const detail = execErr.stderr || execErr.stdout || String(err)
33
+ log.error("/bm-setup failed", err)
34
+ return {
35
+ text: `Setup failed:\n${detail.trim()}`,
36
+ }
37
+ }
38
+ },
39
+ })
40
+
9
41
  api.registerCommand({
10
42
  name: "remember",
11
43
  description: "Save something to the Basic Memory knowledge graph",
@@ -2,12 +2,15 @@
2
2
  "id": "openclaw-basic-memory",
3
3
  "kind": "memory",
4
4
  "skills": [
5
- "skills/memory-tasks",
6
- "skills/memory-reflect",
7
5
  "skills/memory-defrag",
8
- "skills/memory-schema",
6
+ "skills/memory-ingest",
7
+ "skills/memory-lifecycle",
9
8
  "skills/memory-metadata-search",
10
- "skills/memory-notes"
9
+ "skills/memory-notes",
10
+ "skills/memory-reflect",
11
+ "skills/memory-research",
12
+ "skills/memory-schema",
13
+ "skills/memory-tasks"
11
14
  ],
12
15
  "uiHints": {
13
16
  "project": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basicmemory/openclaw-basic-memory",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Basic Memory plugin for OpenClaw — local-first knowledge graph for agent memory",
6
6
  "license": "MIT",
@@ -35,6 +35,7 @@
35
35
  "schema/task-schema.ts",
36
36
  "schema/conversation-schema.ts",
37
37
  "skills/",
38
+ "scripts/fetch-skills.ts",
38
39
  "scripts/setup-bm.sh",
39
40
  "openclaw.plugin.json",
40
41
  "README.md",
@@ -55,6 +56,8 @@
55
56
  "check-types": "tsc --noEmit",
56
57
  "lint": "bunx @biomejs/biome ci .",
57
58
  "lint:fix": "bunx @biomejs/biome check --write .",
59
+ "fetch-skills": "bun scripts/fetch-skills.ts",
60
+ "prepack": "bun scripts/fetch-skills.ts",
58
61
  "test": "bun test",
59
62
  "test:int": "BM_INTEGRATION=1 BM_BIN=${BM_BIN:-./scripts/bm-local.sh} bun test integration --timeout 120000",
60
63
  "test:coverage": "bun test --coverage",
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Fetch all memory-* skills from basicmachines-co/basic-memory-skills.
3
+ *
4
+ * Auto-discovers skill directories via GitHub Contents API, downloads each
5
+ * SKILL.md, writes to skills/<dir>/SKILL.md, and generates skills/manifest.json.
6
+ *
7
+ * Env vars:
8
+ * GITHUB_TOKEN — optional, avoids 60 req/hr unauthenticated rate limit
9
+ * SKILLS_BRANCH — branch to fetch from (default: "main")
10
+ */
11
+
12
+ import { mkdirSync, writeFileSync } from "node:fs"
13
+ import { dirname, resolve } from "node:path"
14
+ import { fileURLToPath } from "node:url"
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url))
17
+ const SKILLS_DIR = resolve(__dirname, "..", "skills")
18
+
19
+ const REPO = "basicmachines-co/basic-memory-skills"
20
+ const BRANCH = process.env.SKILLS_BRANCH ?? "main"
21
+ const TOKEN = process.env.GITHUB_TOKEN
22
+
23
+ interface GitHubEntry {
24
+ name: string
25
+ type: "file" | "dir"
26
+ }
27
+
28
+ interface SkillManifestEntry {
29
+ dir: string
30
+ name: string
31
+ description: string
32
+ }
33
+
34
+ function headers(): Record<string, string> {
35
+ const h: Record<string, string> = {
36
+ Accept: "application/vnd.github.v3+json",
37
+ "User-Agent": "openclaw-basic-memory/fetch-skills",
38
+ }
39
+ if (TOKEN) h.Authorization = `Bearer ${TOKEN}`
40
+ return h
41
+ }
42
+
43
+ async function fetchJSON<T>(url: string): Promise<T> {
44
+ const res = await fetch(url, { headers: headers() })
45
+ if (!res.ok) {
46
+ throw new Error(`GET ${url} → ${res.status} ${res.statusText}`)
47
+ }
48
+ return res.json() as Promise<T>
49
+ }
50
+
51
+ async function fetchText(url: string): Promise<string> {
52
+ const res = await fetch(url, { headers: headers() })
53
+ if (!res.ok) {
54
+ throw new Error(`GET ${url} → ${res.status} ${res.statusText}`)
55
+ }
56
+ return res.text()
57
+ }
58
+
59
+ function parseFrontmatter(md: string): { name: string; description: string } {
60
+ const match = md.match(/^---\r?\n([\s\S]*?)\r?\n---/)
61
+ if (!match) throw new Error("SKILL.md missing YAML frontmatter")
62
+
63
+ const yaml = match[1]
64
+ const name = yaml
65
+ .match(/^name:\s*(.+)$/m)?.[1]
66
+ ?.trim()
67
+ .replace(/^["']|["']$/g, "")
68
+ const description = yaml
69
+ .match(/^description:\s*(.+)$/m)?.[1]
70
+ ?.trim()
71
+ .replace(/^["']|["']$/g, "")
72
+
73
+ if (!name) throw new Error("Frontmatter missing 'name'")
74
+ if (!description) throw new Error("Frontmatter missing 'description'")
75
+
76
+ return { name, description }
77
+ }
78
+
79
+ async function main() {
80
+ console.log(`Fetching skills from ${REPO}@${BRANCH}`)
81
+
82
+ // 1. Discover all memory-* directories
83
+ const contentsUrl = `https://api.github.com/repos/${REPO}/contents?ref=${BRANCH}`
84
+ const entries = await fetchJSON<GitHubEntry[]>(contentsUrl)
85
+ const skillDirs = entries
86
+ .filter((e) => e.type === "dir" && e.name.startsWith("memory-"))
87
+ .map((e) => e.name)
88
+ .sort()
89
+
90
+ if (skillDirs.length === 0) {
91
+ throw new Error("No memory-* directories found in repo")
92
+ }
93
+
94
+ console.log(`Found ${skillDirs.length} skills: ${skillDirs.join(", ")}`)
95
+
96
+ // 2. Download each SKILL.md and parse frontmatter
97
+ const manifest: SkillManifestEntry[] = []
98
+
99
+ const results = await Promise.all(
100
+ skillDirs.map(async (dir) => {
101
+ const rawUrl = `https://raw.githubusercontent.com/${REPO}/${BRANCH}/${dir}/SKILL.md`
102
+ const content = await fetchText(rawUrl)
103
+ const meta = parseFrontmatter(content)
104
+ return { dir, content, meta }
105
+ }),
106
+ )
107
+
108
+ // 3. Write files and build manifest
109
+ for (const { dir, content, meta } of results) {
110
+ const outDir = resolve(SKILLS_DIR, dir)
111
+ mkdirSync(outDir, { recursive: true })
112
+ writeFileSync(resolve(outDir, "SKILL.md"), content)
113
+ manifest.push({ dir, name: meta.name, description: meta.description })
114
+ console.log(` ✓ ${dir}`)
115
+ }
116
+
117
+ // Sort manifest by dir name for deterministic output
118
+ manifest.sort((a, b) => a.dir.localeCompare(b.dir))
119
+
120
+ // 4. Write manifest
121
+ mkdirSync(SKILLS_DIR, { recursive: true })
122
+ writeFileSync(
123
+ resolve(SKILLS_DIR, "manifest.json"),
124
+ `${JSON.stringify(manifest, null, 2)}\n`,
125
+ )
126
+
127
+ console.log(`\nWrote ${manifest.length} skills + manifest.json to skills/`)
128
+ }
129
+
130
+ main().catch((err) => {
131
+ console.error("Fatal:", err.message)
132
+ process.exit(1)
133
+ })
@@ -0,0 +1,47 @@
1
+ [
2
+ {
3
+ "dir": "memory-defrag",
4
+ "name": "memory-defrag",
5
+ "description": "Defragment and reorganize agent memory files: split bloated files, merge duplicates, remove stale information, and restructure the memory hierarchy. Use when memory files have grown unwieldy, contain redundancies, or need reorganization. Run periodically (weekly) or on demand."
6
+ },
7
+ {
8
+ "dir": "memory-ingest",
9
+ "name": "memory-ingest",
10
+ "description": "Process unstructured external input (meeting transcripts, conversation logs, pasted documents) into structured Basic Memory entities. Extracts entities, searches for existing matches, proposes new entities with approval, creates notes with observations and relations, and captures action items."
11
+ },
12
+ {
13
+ "dir": "memory-lifecycle",
14
+ "name": "memory-lifecycle",
15
+ "description": "Manage entity status transitions in Basic Memory: archive completed work, move notes between status folders, update frontmatter, and handle edge cases. Use when marking items complete, archiving old entities, or managing any folder-based status workflow."
16
+ },
17
+ {
18
+ "dir": "memory-metadata-search",
19
+ "name": "memory-metadata-search",
20
+ "description": "Structured metadata search for Basic Memory: query notes by custom frontmatter fields using equality, range, array, and nested filters. Use when finding notes by status, priority, confidence, or any custom YAML field rather than free-text content."
21
+ },
22
+ {
23
+ "dir": "memory-notes",
24
+ "name": "memory-notes",
25
+ "description": "How to write well-structured Basic Memory notes: frontmatter, observations with semantic categories, relations with wiki-links, and best practices for building a rich knowledge graph. Use when creating or improving notes."
26
+ },
27
+ {
28
+ "dir": "memory-reflect",
29
+ "name": "memory-reflect",
30
+ "description": "Sleep-time memory reflection: review recent conversations and daily notes, extract insights, and consolidate into long-term memory. Use when triggered by cron, heartbeat, or explicit request to reflect on recent activity. Runs as background processing to improve memory quality over time."
31
+ },
32
+ {
33
+ "dir": "memory-research",
34
+ "name": "memory-research",
35
+ "description": "Research an external subject using web search, synthesize findings into a structured Basic Memory entity. Use when asked to research a company, person, technology, or topic — or when a bare name or URL is provided that implies a research request."
36
+ },
37
+ {
38
+ "dir": "memory-schema",
39
+ "name": "memory-schema",
40
+ "description": "Schema lifecycle management for Basic Memory: discover unschemaed notes, infer schemas, create and edit schema definitions, validate notes, and detect drift. Use when working with structured note types (Task, Person, Meeting, etc.) to maintain consistency across the knowledge graph."
41
+ },
42
+ {
43
+ "dir": "memory-tasks",
44
+ "name": "memory-tasks",
45
+ "description": "Task management via Basic Memory schemas: create, track, and resume structured tasks that survive context compaction. Uses BM's schema system for uniform notes queryable through the knowledge graph."
46
+ }
47
+ ]
@@ -78,7 +78,6 @@ After changes:
78
78
 
79
79
  ## Guidelines
80
80
 
81
- - **Back up first.** If git is available, commit before defragging. If not, note the pre-defrag state.
82
81
  - **Preserve raw daily notes.** Don't delete or modify `memory/YYYY-MM-DD.md` files — they're the audit trail.
83
82
  - **Target 15-25 focused files.** Too few means bloated files; too many means fragmentation. Aim for the sweet spot.
84
83
  - **File names should be scannable.** Use descriptive names: `people.md`, `project-status.md`, `competitive-landscape.md` — not `notes-2.md`.