@basicmemory/openclaw-basic-memory 0.24.1 → 1.0.0

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.
Files changed (87) hide show
  1. package/README.md +24 -12
  2. package/bm-client.ts +302 -112
  3. package/commands/cli.ts +5 -6
  4. package/commands/slash.ts +4 -4
  5. package/context-engine/basic-memory-context-engine.ts +74 -9
  6. package/context-engine/types.ts +21 -0
  7. package/dist/bm-client.d.ts +61 -12
  8. package/dist/bm-client.js +250 -101
  9. package/dist/commands/cli.d.ts +1 -1
  10. package/dist/commands/cli.js +5 -5
  11. package/dist/commands/slash.d.ts +1 -1
  12. package/dist/commands/slash.js +4 -3
  13. package/dist/context-engine/basic-memory-context-engine.d.ts +20 -1
  14. package/dist/context-engine/basic-memory-context-engine.js +50 -2
  15. package/dist/context-engine/types.d.ts +9 -0
  16. package/dist/context-engine/types.js +1 -0
  17. package/dist/index.d.ts +10 -4
  18. package/dist/index.js +79 -69
  19. package/dist/memory/basic-memory-capability.d.ts +19 -0
  20. package/dist/memory/basic-memory-capability.js +190 -0
  21. package/dist/tools/build-context.d.ts +1 -1
  22. package/dist/tools/build-context.js +12 -4
  23. package/dist/tools/delete-note.d.ts +1 -1
  24. package/dist/tools/delete-note.js +2 -1
  25. package/dist/tools/edit-note.d.ts +1 -1
  26. package/dist/tools/edit-note.js +22 -12
  27. package/dist/tools/list-memory-projects.d.ts +1 -1
  28. package/dist/tools/list-memory-projects.js +2 -1
  29. package/dist/tools/list-workspaces.d.ts +1 -1
  30. package/dist/tools/list-workspaces.js +4 -4
  31. package/dist/tools/move-note.d.ts +1 -1
  32. package/dist/tools/move-note.js +2 -1
  33. package/dist/tools/read-note.d.ts +1 -1
  34. package/dist/tools/read-note.js +2 -1
  35. package/dist/tools/schema-diff.d.ts +1 -1
  36. package/dist/tools/schema-diff.js +2 -1
  37. package/dist/tools/schema-infer.d.ts +1 -1
  38. package/dist/tools/schema-infer.js +2 -1
  39. package/dist/tools/schema-validate.d.ts +1 -1
  40. package/dist/tools/schema-validate.js +2 -1
  41. package/dist/tools/search-notes.d.ts +1 -1
  42. package/dist/tools/search-notes.js +13 -6
  43. package/dist/tools/write-note.d.ts +1 -1
  44. package/dist/tools/write-note.js +11 -12
  45. package/index.ts +93 -84
  46. package/memory/basic-memory-capability.ts +253 -0
  47. package/openclaw.plugin.json +0 -2
  48. package/package.json +6 -5
  49. package/skills/manifest.json +5 -0
  50. package/skills/memory-agents/SKILL.md +23 -10
  51. package/skills/memory-capture/SKILL.md +29 -18
  52. package/skills/memory-comark/SKILL.md +17 -6
  53. package/skills/memory-continue/SKILL.md +47 -27
  54. package/skills/memory-curate/SKILL.md +40 -24
  55. package/skills/memory-defrag/SKILL.md +18 -7
  56. package/skills/memory-doctor/SKILL.md +93 -0
  57. package/skills/memory-ingest/SKILL.md +33 -17
  58. package/skills/memory-lifecycle/SKILL.md +35 -22
  59. package/skills/memory-literary-analysis/SKILL.md +112 -75
  60. package/skills/memory-metadata-search/SKILL.md +39 -28
  61. package/skills/memory-notes/SKILL.md +75 -50
  62. package/skills/memory-okf/SKILL.md +27 -15
  63. package/skills/memory-onboarding/SKILL.md +20 -9
  64. package/skills/memory-onboarding/references/assistant-setup.md +1 -1
  65. package/skills/memory-onboarding/references/conventions.md +4 -4
  66. package/skills/memory-onboarding/references/domain-playbooks.md +1 -1
  67. package/skills/memory-onboarding/references/schema-guide.md +9 -8
  68. package/skills/memory-quest/SKILL.md +18 -7
  69. package/skills/memory-reflect/SKILL.md +25 -13
  70. package/skills/memory-research/SKILL.md +28 -13
  71. package/skills/memory-schema/SKILL.md +36 -24
  72. package/skills/memory-tasks/SKILL.md +34 -22
  73. package/tools/build-context.ts +14 -5
  74. package/tools/delete-note.ts +2 -2
  75. package/tools/edit-note.ts +27 -14
  76. package/tools/list-memory-projects.ts +6 -2
  77. package/tools/list-workspaces.ts +8 -5
  78. package/tools/move-note.ts +2 -2
  79. package/tools/read-note.ts +2 -2
  80. package/tools/schema-diff.ts +2 -2
  81. package/tools/schema-infer.ts +2 -2
  82. package/tools/schema-validate.ts +2 -2
  83. package/tools/search-notes.ts +18 -7
  84. package/tools/write-note.ts +12 -12
  85. package/dist/tools/memory-provider.d.ts +0 -15
  86. package/dist/tools/memory-provider.js +0 -275
  87. package/tools/memory-provider.ts +0 -336
@@ -1,13 +1,16 @@
1
1
  import { Type } from "@sinclair/typebox"
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
3
- import type { BmClient, WorkspaceResult } from "../bm-client.ts"
3
+ import {
4
+ type BmClient,
5
+ describeToolError,
6
+ type WorkspaceResult,
7
+ } from "../bm-client.ts"
4
8
  import { log } from "../logger.ts"
5
9
 
6
10
  function formatWorkspace(ws: WorkspaceResult, idx: number): string {
7
- const subscription = ws.has_active_subscription ? "active" : "none"
8
11
  return (
9
12
  `${idx + 1}. **${ws.name}**\n` +
10
- ` Type: ${ws.workspace_type} | Role: ${ws.role} | Subscription: ${subscription}`
13
+ ` Type: ${ws.workspace_type ?? "unknown"}`
11
14
  )
12
15
  }
13
16
 
@@ -20,7 +23,7 @@ export function registerWorkspaceListTool(
20
23
  name: "list_workspaces",
21
24
  label: "List Workspaces",
22
25
  description:
23
- "List all Basic Memory workspaces (personal and organization) accessible to this user. " +
26
+ "List workspaces represented by accessible Basic Memory projects. " +
24
27
  "Use it to find the workspace to pass to list_memory_projects when the same " +
25
28
  "project name may exist in more than one workspace.",
26
29
  parameters: Type.Object({}),
@@ -65,7 +68,7 @@ export function registerWorkspaceListTool(
65
68
  content: [
66
69
  {
67
70
  type: "text" as const,
68
- text: "Failed to list workspaces. Is Basic Memory running? Check logs for details.",
71
+ text: `Failed to list workspaces: ${describeToolError(err)}`,
69
72
  },
70
73
  ],
71
74
  details: {
@@ -1,6 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox"
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
3
- import type { BmClient } from "../bm-client.ts"
3
+ import { type BmClient, describeNoteError } from "../bm-client.ts"
4
4
  import { log } from "../logger.ts"
5
5
 
6
6
  export function registerMoveTool(
@@ -62,7 +62,7 @@ export function registerMoveTool(
62
62
  content: [
63
63
  {
64
64
  type: "text" as const,
65
- text: `Failed to move "${params.identifier}". It may not exist.`,
65
+ text: `Failed to move "${params.identifier}": ${describeNoteError(err)}`,
66
66
  },
67
67
  ],
68
68
  details: {
@@ -1,6 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox"
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
3
- import type { BmClient } from "../bm-client.ts"
3
+ import { type BmClient, describeNoteError } from "../bm-client.ts"
4
4
  import { log } from "../logger.ts"
5
5
 
6
6
  export function registerReadTool(
@@ -86,7 +86,7 @@ export function registerReadTool(
86
86
  content: [
87
87
  {
88
88
  type: "text" as const,
89
- text: `Could not read note "${params.identifier}". It may not exist yet.`,
89
+ text: `Could not read note "${params.identifier}": ${describeNoteError(err)}`,
90
90
  },
91
91
  ],
92
92
  details: {
@@ -1,6 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox"
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
3
- import type { BmClient } from "../bm-client.ts"
3
+ import { type BmClient, describeToolError } from "../bm-client.ts"
4
4
  import { log } from "../logger.ts"
5
5
 
6
6
  export function registerSchemaDiffTool(
@@ -93,7 +93,7 @@ export function registerSchemaDiffTool(
93
93
  content: [
94
94
  {
95
95
  type: "text" as const,
96
- text: "Schema diff failed. Check logs for details.",
96
+ text: `Schema diff failed: ${describeToolError(err)}`,
97
97
  },
98
98
  ],
99
99
  details: {
@@ -1,6 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox"
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
3
- import type { BmClient } from "../bm-client.ts"
3
+ import { type BmClient, describeToolError } from "../bm-client.ts"
4
4
  import { log } from "../logger.ts"
5
5
 
6
6
  export function registerSchemaInferTool(
@@ -91,7 +91,7 @@ export function registerSchemaInferTool(
91
91
  content: [
92
92
  {
93
93
  type: "text" as const,
94
- text: "Schema inference failed. Check logs for details.",
94
+ text: `Schema inference failed: ${describeToolError(err)}`,
95
95
  },
96
96
  ],
97
97
  details: {
@@ -1,6 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox"
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
3
- import type { BmClient } from "../bm-client.ts"
3
+ import { type BmClient, describeToolError } from "../bm-client.ts"
4
4
  import { log } from "../logger.ts"
5
5
 
6
6
  export function registerSchemaValidateTool(
@@ -89,7 +89,7 @@ export function registerSchemaValidateTool(
89
89
  content: [
90
90
  {
91
91
  type: "text" as const,
92
- text: "Schema validation failed. Check logs for details.",
92
+ text: `Schema validation failed: ${describeToolError(err)}`,
93
93
  },
94
94
  ],
95
95
  details: {
@@ -1,8 +1,16 @@
1
1
  import { Type } from "@sinclair/typebox"
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
3
- import type { BmClient } from "../bm-client.ts"
3
+ import {
4
+ type BmClient,
5
+ describeToolError,
6
+ searchResultText,
7
+ } from "../bm-client.ts"
4
8
  import { log } from "../logger.ts"
5
9
 
10
+ // Long enough to show a whole captured conversation turn from the matched
11
+ // chunk, short enough that ten results stay readable.
12
+ const MAX_PREVIEW_CHARS = 500
13
+
6
14
  export function registerSearchTool(
7
15
  api: OpenClawPluginApi,
8
16
  client: BmClient,
@@ -15,8 +23,9 @@ export function registerSearchTool(
15
23
  "Search the Basic Memory knowledge graph for relevant notes, concepts, and connections. " +
16
24
  "Returns matching notes with titles, content previews, and relevance scores. " +
17
25
  "Optionally filter by frontmatter metadata fields, tags, or status, or target " +
18
- "another project. For a quick cross-source recall that also covers MEMORY.md " +
19
- "and active tasks, use memory_search.",
26
+ "another project. This is the search for long-term memory and earlier " +
27
+ "conversations; memory_search covers only the workspace MEMORY.md and memory/ " +
28
+ "files. Read a result in full with read_note, or follow its links with build_context.",
20
29
  parameters: Type.Object({
21
30
  query: Type.String({ description: "Search query" }),
22
31
  limit: Type.Optional(
@@ -98,10 +107,12 @@ export function registerSearchTool(
98
107
 
99
108
  const text = results
100
109
  .map((r, i) => {
101
- const score = r.score ? ` (${(r.score * 100).toFixed(0)}%)` : ""
102
- const content = r.content ?? ""
110
+ const score = r.score ? ` (score ${r.score.toFixed(2)})` : ""
111
+ const text = searchResultText(r)
103
112
  const preview =
104
- content.length > 200 ? `${content.slice(0, 200)}...` : content
113
+ text.length > MAX_PREVIEW_CHARS
114
+ ? `${text.slice(0, MAX_PREVIEW_CHARS)}...`
115
+ : text
105
116
  return `${i + 1}. **${r.title}**${score}\n ${preview}`
106
117
  })
107
118
  .join("\n\n")
@@ -128,7 +139,7 @@ export function registerSearchTool(
128
139
  content: [
129
140
  {
130
141
  type: "text" as const,
131
- text: "Search failed. Is Basic Memory running? Check logs for details.",
142
+ text: `Search failed: ${describeToolError(err)}`,
132
143
  },
133
144
  ],
134
145
  details: {
@@ -1,7 +1,8 @@
1
1
  import { Type } from "@sinclair/typebox"
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
3
- import type { BmClient } from "../bm-client.ts"
4
3
  import {
4
+ type BmClient,
5
+ describeToolError,
5
6
  NoteAlreadyExistsError,
6
7
  NoteRevisionConflictError,
7
8
  } from "../bm-client.ts"
@@ -73,7 +74,7 @@ export function registerWriteTool(
73
74
  expected_checksum: Type.Optional(
74
75
  Type.String({
75
76
  description:
76
- "Requires overwrite=true. The checksum of the note as you read it; " +
77
+ "Implies overwrite=true. The checksum of the note as you read it; " +
77
78
  "the note is replaced only while it is still that revision.",
78
79
  }),
79
80
  ),
@@ -123,7 +124,9 @@ export function registerWriteTool(
123
124
  const checksumSuffix = note.checksum
124
125
  ? `\nChecksum: ${note.checksum}`
125
126
  : ""
126
- const msg = `Note saved: ${note.title} (${note.permalink})${checksumSuffix}`
127
+ // The description tells agents to cite permalink and file_path, so both
128
+ // must appear in the text the agent reads, not only in details.
129
+ const msg = `Note saved: ${note.title} (${note.permalink})\nFile: ${note.file_path}${checksumSuffix}`
127
130
  return {
128
131
  content: [
129
132
  {
@@ -141,13 +144,10 @@ export function registerWriteTool(
141
144
  } catch (err) {
142
145
  if (err instanceof NoteAlreadyExistsError) {
143
146
  const hint = [
144
- `Note already exists: "${params.title}" (${err.permalink})`,
147
+ err.message,
145
148
  "",
146
- "To update this note, use one of:",
147
- ` - edit_note("${err.permalink}", operation="append", content="...")`,
148
- ` - edit_note("${err.permalink}", operation="replace_section", section="...", content="...")`,
149
- ` - write_note("${params.title}", ..., overwrite=true) to fully replace`,
150
- ` - read_note("${err.permalink}", include_frontmatter=true) to inspect it first; pass its checksum as expected_checksum with overwrite=true to replace only that revision`,
149
+ "Use search_notes to locate the note, then read_note to inspect its current content and checksum.",
150
+ "Use edit_note for incremental changes, or write_note with overwrite=true and expected_checksum to replace the revision you read.",
151
151
  ].join("\n")
152
152
 
153
153
  return {
@@ -163,9 +163,9 @@ export function registerWriteTool(
163
163
  if (err instanceof NoteRevisionConflictError) {
164
164
  const current = err.currentChecksum
165
165
  ? `Its current checksum is ${err.currentChecksum}.`
166
- : "No note exists at that path any more."
166
+ : "Read the note to obtain its current checksum."
167
167
  const text = [
168
- `Note revision conflict: "${params.title}" changed since it was read, so nothing was replaced. ${current}`,
168
+ `Note revision conflict: ${err.message} ${current}`,
169
169
  "",
170
170
  "Read the note again, confirm it should still be replaced, then retry with the new checksum as expected_checksum.",
171
171
  ].join("\n")
@@ -186,7 +186,7 @@ export function registerWriteTool(
186
186
  content: [
187
187
  {
188
188
  type: "text" as const,
189
- text: "Failed to write note. Is Basic Memory running? Check logs for details.",
189
+ text: `Failed to write note: ${describeToolError(err)}`,
190
190
  },
191
191
  ],
192
192
  details: {
@@ -1,15 +0,0 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
2
- import type { BmClient } from "../bm-client.ts";
3
- import type { BasicMemoryConfig } from "../config.ts";
4
- export declare function setWorkspaceDir(dir: string): void;
5
- /**
6
- * Register composited memory_search and memory_get tools.
7
- *
8
- * memory_search queries 3 sources in parallel:
9
- * 1. MEMORY.md — grep/text search
10
- * 2. BM knowledge graph — semantic + FTS search
11
- * 3. Active tasks — memory/tasks/ files with status != done
12
- *
13
- * memory_get reads a specific note by identifier.
14
- */
15
- export declare function registerMemoryProvider(api: OpenClawPluginApi, client: BmClient, cfg: BasicMemoryConfig): void;
@@ -1,275 +0,0 @@
1
- import { readdir, readFile } from "node:fs/promises";
2
- import { join, resolve } from "node:path";
3
- import { Type } from "@sinclair/typebox";
4
- import { log } from "../logger.js";
5
- /**
6
- * Search MEMORY.md for lines matching the query.
7
- * Returns matching lines with 1 line of surrounding context.
8
- */
9
- async function searchMemoryFile(query, workspaceDir, memoryFile) {
10
- try {
11
- const filePath = resolve(workspaceDir, memoryFile);
12
- const content = await readFile(filePath, "utf-8");
13
- const lines = content.split("\n");
14
- const queryLower = query.toLowerCase();
15
- const terms = queryLower.split(/\s+/).filter((t) => t.length > 0);
16
- // Find lines that match any search term
17
- const matchingIndices = new Set();
18
- for (let i = 0; i < lines.length; i++) {
19
- const lineLower = lines[i].toLowerCase();
20
- if (terms.some((term) => lineLower.includes(term))) {
21
- // Add the matching line + 1 line before/after for context
22
- if (i > 0)
23
- matchingIndices.add(i - 1);
24
- matchingIndices.add(i);
25
- if (i < lines.length - 1)
26
- matchingIndices.add(i + 1);
27
- }
28
- }
29
- if (matchingIndices.size === 0)
30
- return "";
31
- // Group consecutive lines into snippets
32
- const sorted = [...matchingIndices].sort((a, b) => a - b);
33
- const snippets = [];
34
- let current = [];
35
- let lastIdx = -2;
36
- for (const idx of sorted) {
37
- if (idx !== lastIdx + 1 && current.length > 0) {
38
- snippets.push(current.join("\n"));
39
- current = [];
40
- }
41
- current.push(`- ${lines[idx]}`);
42
- lastIdx = idx;
43
- }
44
- if (current.length > 0)
45
- snippets.push(current.join("\n"));
46
- return snippets.join("\n…\n");
47
- }
48
- catch {
49
- return "";
50
- }
51
- }
52
- /**
53
- * Search for active tasks via BM knowledge graph.
54
- * Uses search_notes with note_types and status filters for precise querying.
55
- * Falls back to filesystem scan if search fails.
56
- */
57
- async function searchActiveTasks(query, client, workspaceDir, memoryDir) {
58
- // Try structured search first
59
- try {
60
- const results = await client.search(query || undefined, 10, undefined, {
61
- note_types: ["task"],
62
- status: "active",
63
- });
64
- if (results.length > 0) {
65
- const matches = [];
66
- for (const r of results) {
67
- const score = r.score ? ` (${r.score.toFixed(2)})` : "";
68
- const preview = (r.content ?? "").length > 200
69
- ? `${(r.content ?? "").slice(0, 200)}…`
70
- : (r.content ?? "");
71
- matches.push(`- **${r.title}**${score} — ${r.file_path}\n > ${preview.replace(/\n/g, "\n > ")}`);
72
- }
73
- return matches.join("\n\n");
74
- }
75
- }
76
- catch (err) {
77
- log.debug("BM task search failed, falling back to filesystem scan", err);
78
- }
79
- // Fallback: filesystem scan for tasks not yet indexed
80
- try {
81
- const tasksDir = resolve(workspaceDir, memoryDir, "tasks");
82
- let entries;
83
- try {
84
- entries = (await readdir(tasksDir, {
85
- withFileTypes: true,
86
- }));
87
- }
88
- catch {
89
- return "";
90
- }
91
- const queryLower = query.toLowerCase();
92
- const terms = queryLower.split(/\s+/).filter((t) => t.length > 0);
93
- const matches = [];
94
- for (const entry of entries) {
95
- if (!entry.isFile() || !entry.name.endsWith(".md"))
96
- continue;
97
- const filePath = join(entry.parentPath ?? tasksDir, entry.name);
98
- const content = await readFile(filePath, "utf-8");
99
- const statusMatch = content.match(/status:\s*(\S+)/);
100
- const status = statusMatch?.[1] ?? "unknown";
101
- if (status === "done")
102
- continue;
103
- const contentLower = content.toLowerCase();
104
- const matchesQuery = terms.length === 0 || terms.some((term) => contentLower.includes(term));
105
- if (!matchesQuery)
106
- continue;
107
- const titleMatch = content.match(/title:\s*(.+)/);
108
- const title = titleMatch?.[1] ?? entry.name.replace(/\.md$/, "");
109
- const stepMatch = content.match(/current_step:\s*(\S+)/);
110
- const currentStep = stepMatch?.[1] ?? "?";
111
- const contextMatch = content.match(/## Context\s*\n([\s\S]*?)(?=\n##|\n---|$)/);
112
- const context = contextMatch?.[1]?.trim().slice(0, 150) ?? "";
113
- matches.push(`- **${title}** (status: ${status}, step: ${currentStep})\n ${context}`);
114
- }
115
- return matches.join("\n");
116
- }
117
- catch {
118
- return "";
119
- }
120
- }
121
- // Store workspace dir for use in memory_search (set during service start)
122
- let _workspaceDir = "";
123
- export function setWorkspaceDir(dir) {
124
- _workspaceDir = dir;
125
- }
126
- /**
127
- * Register composited memory_search and memory_get tools.
128
- *
129
- * memory_search queries 3 sources in parallel:
130
- * 1. MEMORY.md — grep/text search
131
- * 2. BM knowledge graph — semantic + FTS search
132
- * 3. Active tasks — memory/tasks/ files with status != done
133
- *
134
- * memory_get reads a specific note by identifier.
135
- */
136
- export function registerMemoryProvider(api, client, cfg) {
137
- // --- composited memory_search ---
138
- api.registerTool({
139
- name: "memory_search",
140
- label: "Memory Search",
141
- description: "Search across all memory sources: MEMORY.md (working memory), " +
142
- "Basic Memory knowledge graph (long-term archive), and active tasks. " +
143
- "Returns composited results from all sources. Use it for broad recall; use " +
144
- "search_notes when you need metadata, tag, or status filters or a specific project.",
145
- parameters: Type.Object({
146
- query: Type.String({
147
- description: "Search query — natural language or keywords",
148
- }),
149
- }),
150
- async execute(_toolCallId, params) {
151
- log.debug(`memory_search: query="${params.query}"`);
152
- const workspaceDir = _workspaceDir || process.cwd();
153
- // Query all 3 sources in parallel
154
- const [memoryMd, bmResults, taskResults] = await Promise.all([
155
- searchMemoryFile(params.query, workspaceDir, cfg.memoryFile),
156
- client
157
- .search(params.query, 5)
158
- .then((results) => {
159
- if (results.length === 0)
160
- return "";
161
- return results
162
- .map((r) => {
163
- const score = r.score ? ` (${r.score.toFixed(2)})` : "";
164
- const preview = (r.content ?? "").length > 200
165
- ? `${(r.content ?? "").slice(0, 200)}…`
166
- : (r.content ?? "");
167
- const source = r.file_path || r.permalink;
168
- return `- ${source}${score}\n > ${preview.replace(/\n/g, "\n > ")}`;
169
- })
170
- .join("\n\n");
171
- })
172
- .catch((err) => {
173
- log.error("BM search failed in composited search", err);
174
- return "(search unavailable)";
175
- }),
176
- searchActiveTasks(params.query, client, workspaceDir, cfg.memoryDir),
177
- ]);
178
- // Build composited result
179
- const sections = [];
180
- if (memoryMd) {
181
- sections.push(`## ${cfg.memoryFile}\n${memoryMd}`);
182
- }
183
- if (bmResults) {
184
- sections.push(`## Knowledge Graph (${cfg.memoryDir})\n${bmResults}`);
185
- }
186
- if (taskResults) {
187
- sections.push(`## Active Tasks\n${taskResults}`);
188
- }
189
- if (sections.length === 0) {
190
- return {
191
- content: [
192
- {
193
- type: "text",
194
- text: "No matches found across memory sources.",
195
- },
196
- ],
197
- details: {
198
- query: params.query,
199
- sectionCount: 0,
200
- hasMemoryFileMatches: false,
201
- hasKnowledgeGraphMatches: false,
202
- hasTaskMatches: false,
203
- },
204
- };
205
- }
206
- return {
207
- content: [
208
- {
209
- type: "text",
210
- text: sections.join("\n\n"),
211
- },
212
- ],
213
- details: {
214
- query: params.query,
215
- sectionCount: sections.length,
216
- hasMemoryFileMatches: memoryMd.length > 0,
217
- hasKnowledgeGraphMatches: bmResults.length > 0,
218
- hasTaskMatches: taskResults.length > 0,
219
- },
220
- };
221
- },
222
- }, { names: ["memory_search"] });
223
- // --- memory_get (unchanged) ---
224
- api.registerTool({
225
- name: "memory_get",
226
- label: "Memory Get",
227
- description: "Read a specific note from the knowledge graph by title, permalink, or path. " +
228
- "Returns the full note content. Use after memory_search to read a specific result in full.",
229
- parameters: Type.Object({
230
- path: Type.String({
231
- description: "Note identifier — title, permalink, memory:// URL, or file path",
232
- }),
233
- from: Type.Optional(Type.Number({
234
- description: "Starting line number (ignored — included for compatibility)",
235
- })),
236
- lines: Type.Optional(Type.Number({
237
- description: "Number of lines to read (ignored — included for compatibility)",
238
- })),
239
- }),
240
- async execute(_toolCallId, params) {
241
- log.debug(`memory_get: path="${params.path}"`);
242
- try {
243
- const note = await client.readNote(params.path);
244
- return {
245
- content: [
246
- {
247
- type: "text",
248
- text: `# ${note.title}\n\n${note.content}`,
249
- },
250
- ],
251
- details: {
252
- title: note.title,
253
- permalink: note.permalink,
254
- file_path: note.file_path,
255
- },
256
- };
257
- }
258
- catch (err) {
259
- log.error("memory_get failed", err);
260
- return {
261
- content: [
262
- {
263
- type: "text",
264
- text: `Could not read "${params.path}". It may not exist in the knowledge graph.`,
265
- },
266
- ],
267
- details: {
268
- path: params.path,
269
- error: "memory_get_failed",
270
- },
271
- };
272
- }
273
- },
274
- }, { names: ["memory_get"] });
275
- }