@mingxy/cerebro 2.3.4 → 2.3.6

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/src/tools.ts CHANGED
@@ -1,455 +1,455 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import type { CerebroClient } from "./client.js";
3
- import { type CerebroPluginConfig, resolveAgentPolicy } from "./config.js";
4
- import { isAutoStoreEnabled, setAutoStoreEnabled } from "./index.js";
5
-
6
- export interface ToolContext {
7
- agentId?: string;
8
- getSessionId: () => string | undefined;
9
- getAgentName?: () => string;
10
- getProjectPath?: () => string | undefined;
11
- config?: Partial<CerebroPluginConfig>;
12
- }
13
-
14
- export function buildTools(client: CerebroClient, containerTags: string[], context: ToolContext) {
15
- function checkPermission(required: "read" | "write"): boolean {
16
- const agentId = context.getAgentName?.() || context.agentId;
17
- if (!agentId || !context.config) return true; // no policy configured → allow
18
- const policy = resolveAgentPolicy(agentId, context.config);
19
- if (policy === "none") return false;
20
- if (policy === "readonly") return required === "read";
21
- return true; // readwrite
22
- }
23
-
24
- function denyMessage(required: "read" | "write"): string {
25
- const agentId = context.getAgentName?.() || context.agentId || "unknown";
26
- const policy = (agentId && context.config) ? resolveAgentPolicy(agentId, context.config) : "readwrite";
27
- return `Permission denied: agent '${agentId}' has '${policy}' policy, but this operation requires '${required}' access`;
28
- }
29
-
30
- return {
31
- memory_store: tool({
32
- description:
33
- "Store a new memory in the user's long-term memory. " +
34
- "Use when the user explicitly asks to remember something, " +
35
- "or when you identify important preferences, facts, or decisions worth preserving. " +
36
- "IMPORTANT: Before calling, you MUST analyze: (1) Which category fits best? (2) Is this project-specific or cross-project? (3) Does it contain sensitive data? (4) Are tags accurate and descriptive? " +
37
- "Every memory MUST have a correct category and at least 1 meaningful tag. " +
38
- "Memories are automatically scoped to the current project via project_path. " +
39
- "Set scope='global' for cross-project memories that should be visible everywhere. " +
40
- "Private memories (visibility='private') are always agent-scoped and not bound to any project — use for sensitive data.",
41
- args: {
42
- content: tool.schema.string().describe(
43
- "The information to remember. MUST be: atomic (one fact per memory), complete (self-contained without context), and precise (no ambiguity). " +
44
- "BAD: 'fixed some bugs'. GOOD: 'Fixed memory_type validation bug in memory.rs:1480 - LLM returns illegal \"pinned\" value, added match guard to normalize to WORK/EMOTIONAL fallback'."
45
- ),
46
- tags: tool.schema
47
- .array(tool.schema.string())
48
- .optional()
49
- .describe(
50
- "REQUIRED. At least 1 tag in snake_case. Tags describe the memory's topic/domain for future retrieval. " +
51
- "Examples: rust_backend, memory_system, bug_fix, user_preference, project_config. " +
52
- "NEVER leave empty — if unsure, use a broad tag like the project name or topic area."
53
- ),
54
- source: tool.schema
55
- .string()
56
- .describe("Origin context, e.g. 'conversation', 'code-review', 'user-input', 'debugging', 'architecture-decision'"),
57
- scope: tool.schema
58
- .string()
59
- .optional()
60
- .describe(
61
- "'project' (default) = only visible in current project context. 'global' = visible across all projects. " +
62
- "Rule: if the memory applies generally (user preferences, general knowledge, cross-project patterns) use 'global'. " +
63
- "If it's specific to one project's code/architecture, use 'project'."
64
- ),
65
- visibility: tool.schema
66
- .string()
67
- .optional()
68
- .describe(
69
- "'global' (default) = all agents can see and recall this memory. 'private' = ONLY the current agent can see it. " +
70
- "MUST use 'private' when content contains: passwords, API keys, tokens, database credentials, SSH keys, personal information (phone, email, address), " +
71
- "internal company details, or anything the user would NOT want other agents to access. " +
72
- "WARNING: private memories are invisible to ALL other agents — if in doubt, ask the user. " +
73
- "Do NOT overuse 'private' for normal work notes — default 'global' is correct for most cases."
74
- ),
75
- category: tool.schema
76
- .enum(["cases", "preferences", "entities", "events", "profile", "patterns"])
77
- .optional()
78
- .describe(
79
- "Memory category. MUST be one of these exact values (lowercase): " +
80
- "'cases' (default) = work records, bug fixes, architecture decisions; " +
81
- "'preferences' = user likes/dislikes, coding style, tool choices; " +
82
- "'entities' = projects, tools, people, concepts; " +
83
- "'events' = time-bound milestones (deployments, releases, incidents); " +
84
- "'profile' = user identity traits (role, skills, team membership); " +
85
- "'patterns' = workflows, methodologies, best practices. " +
86
- "When in doubt, omit this field (defaults to 'cases')."
87
- ),
88
- },
89
- async execute(args) {
90
- if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
91
- const allTags = [...containerTags, ...(args.tags ?? [])];
92
- const effectiveAgentId = context.getAgentName?.() || context.agentId;
93
- const result = await client.createMemory(
94
- args.content,
95
- allTags,
96
- args.source,
97
- args.scope ?? "project",
98
- effectiveAgentId,
99
- context.getSessionId(),
100
- args.visibility,
101
- args.category,
102
- context.getProjectPath?.(),
103
- );
104
- if (!result) return JSON.stringify({ ok: false, error: "The Cerebro server may be unavailable." });
105
- return JSON.stringify({ ok: true, id: result.id, tags: result.tags });
106
- },
107
- }),
108
-
109
- memory_search: tool({
110
- description:
111
- "Search the user's long-term memory by semantic similarity. " +
112
- "Use to recall previously stored preferences, facts, or context. " +
113
- "Searches are automatically filtered by the current project_path. " +
114
- "Global-scope memories and memories without a project_path are always included in results. " +
115
- "Private memories are visible only to the creating agent.",
116
- args: {
117
- query: tool.schema.string().describe("Natural-language search query"),
118
- limit: tool.schema
119
- .number()
120
- .optional()
121
- .describe("Max results to return (default 10)"),
122
- scope: tool.schema
123
- .string()
124
- .optional()
125
- .describe("Optional scope filter"),
126
- },
127
- async execute(args) {
128
- if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
129
- const results = await client.searchMemories(
130
- args.query,
131
- args.limit ?? 10,
132
- args.scope,
133
- containerTags,
134
- context.getProjectPath?.(),
135
- );
136
- if (results.length === 0) return JSON.stringify({ ok: true, count: 0, results: [] });
137
- const items = results.map((r) => ({
138
- id: r.memory.id,
139
- score: r.score,
140
- content: r.memory.content.slice(0, 200),
141
- }));
142
- return JSON.stringify({ ok: true, count: results.length, results: items });
143
- },
144
- }),
145
-
146
- memory_get: tool({
147
- description:
148
- "Retrieve a specific memory by its ID. " +
149
- "Use when a recalled memory's content was truncated (e.g. medium relevance summary) " +
150
- "and you need the full details, or when you see [rel:<id>] markers in injected context " +
151
- "and want to fetch related memories.",
152
- args: {
153
- id: tool.schema.string().describe("Memory ID"),
154
- },
155
- async execute(args) {
156
- if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
157
- const memory = await client.getMemory(args.id);
158
- if (!memory) return JSON.stringify({ ok: false, error: "not found" });
159
- return JSON.stringify({ ok: true, memory });
160
- },
161
- }),
162
-
163
- memory_update: tool({
164
- description:
165
- "Update the content or tags of an existing memory. " +
166
- "Use when information needs correction or enrichment.",
167
- args: {
168
- id: tool.schema.string().describe("Memory ID to update"),
169
- content: tool.schema.string().describe("New content"),
170
- tags: tool.schema
171
- .array(tool.schema.string())
172
- .optional()
173
- .describe("Replacement tags"),
174
- },
175
- async execute(args) {
176
- if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
177
- const result = await client.updateMemory(
178
- args.id,
179
- args.content,
180
- args.tags,
181
- );
182
- if (!result) return JSON.stringify({ ok: false, error: `Failed to update memory ${args.id}` });
183
- return JSON.stringify({ ok: true, id: args.id });
184
- },
185
- }),
186
-
187
- memory_profile: tool({
188
- description:
189
- "Get the user profile synthesized from stored memories. Shows preferences, patterns, and key information.",
190
- args: {},
191
- async execute() {
192
- if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
193
- const preferences = await client.getProfile();
194
- if (preferences.length === 0) return JSON.stringify({ ok: true, count: 0, preferences: [] });
195
- return JSON.stringify({ ok: true, count: preferences.length, preferences });
196
- },
197
- }),
198
-
199
- memory_profile_stats: tool({
200
- description:
201
- "View user profile statistics — total preferences, slot distribution, induction run counts, etc.",
202
- args: {},
203
- async execute() {
204
- if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
205
- const stats = await client.getProfileStats();
206
- return JSON.stringify({ ok: true, stats });
207
- },
208
- }),
209
-
210
- memory_list: tool({
211
- description:
212
- "List the most recent memories. Use to browse what's been remembered without a search query.",
213
- args: {
214
- limit: tool.schema
215
- .number()
216
- .optional()
217
- .describe("Max memories to return (default: 20)"),
218
- },
219
- async execute(args) {
220
- if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
221
- const memories = await client.listRecent(args.limit ?? 20);
222
- if (memories.length === 0) return JSON.stringify({ ok: true, count: 0, memories: [] });
223
- const items = memories.map((m) => ({
224
- id: m.id,
225
- content: m.content.slice(0, 120),
226
- category: m.category,
227
- tags: m.tags,
228
- }));
229
- return JSON.stringify({ ok: true, count: memories.length, memories: items });
230
- },
231
- }),
232
-
233
- memory_ingest: tool({
234
- description:
235
- "Ingest conversation messages for intelligent extraction. The system extracts atomic facts, deduplicates, and reconciles with existing memories. " +
236
- "Extracted memories are automatically scoped to the current project via project_path. " +
237
- "Global-scope memories are visible across all projects.",
238
- args: {
239
- messages: tool.schema
240
- .array(
241
- tool.schema.object({
242
- role: tool.schema.string().describe("Message role: user, assistant, or system"),
243
- content: tool.schema.string().describe("Message content"),
244
- }),
245
- )
246
- .describe("Conversation messages to ingest"),
247
- mode: tool.schema
248
- .enum(["smart", "raw"])
249
- .optional()
250
- .describe("Extraction mode: 'smart' (default) or 'raw'"),
251
- tags: tool.schema
252
- .array(tool.schema.string())
253
- .optional()
254
- .describe("Tags to apply to extracted memories"),
255
- session_id: tool.schema
256
- .string()
257
- .optional()
258
- .describe("Session ID to associate with the ingestion"),
259
- },
260
- async execute(args) {
261
- if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
262
- const effectiveAgentId = context.getAgentName?.() || context.agentId;
263
- const result = await client.ingestMessages(args.messages, {
264
- mode: args.mode ?? "smart",
265
- tags: args.tags,
266
- sessionId: args.session_id,
267
- agentId: effectiveAgentId,
268
- projectPath: context.getProjectPath?.(),
269
- });
270
- if (result === null) return JSON.stringify({ ok: false, error: "Ingestion failed" });
271
- return JSON.stringify({ ok: true, result });
272
- },
273
- }),
274
-
275
- memory_stats: tool({
276
- description:
277
- "Get statistics about stored memories — counts by category, type, tier, and timeline.",
278
- args: {},
279
- async execute() {
280
- if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
281
- const stats = await client.getStats();
282
- if (!stats) return JSON.stringify({ ok: false, error: "Failed to get stats" });
283
- return JSON.stringify({ ok: true, stats });
284
- },
285
- }),
286
-
287
- memory_delete: tool({
288
- description:
289
- "Delete a memory by ID. Use when the user asks to forget something.",
290
- args: {
291
- id: tool.schema.string().describe("Memory ID to delete"),
292
- },
293
- async execute(args) {
294
- if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
295
- try {
296
- await client.deleteMemory(args.id);
297
- return JSON.stringify({ ok: true, id: args.id });
298
- } catch {
299
- return JSON.stringify({ ok: false, error: `Failed to delete memory ${args.id}` });
300
- }
301
- },
302
- }),
303
-
304
- space_create: tool({
305
- description:
306
- "Create a shared space (team or organization) for sharing memories across users and agents.",
307
- args: {
308
- name: tool.schema.string().describe("Name of the space"),
309
- space_type: tool.schema
310
- .string()
311
- .describe("Type of space: 'team' or 'organization'"),
312
- members: tool.schema
313
- .array(
314
- tool.schema.object({
315
- user_id: tool.schema.string().describe("User/tenant ID to add"),
316
- role: tool.schema.string().describe("Member role: admin, member, or reader"),
317
- }),
318
- )
319
- .optional()
320
- .describe("Initial members to add"),
321
- },
322
- async execute(args) {
323
- if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
324
- const result = await client.createSpace(
325
- args.name,
326
- args.space_type,
327
- args.members,
328
- );
329
- if (!result) return JSON.stringify({ ok: false, error: "Failed to create space" });
330
- return JSON.stringify({ ok: true, space: result });
331
- },
332
- }),
333
-
334
- space_list: tool({
335
- description:
336
- "List all spaces you own or are a member of.",
337
- args: {},
338
- async execute() {
339
- if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
340
- const spaces = await client.listSpaces();
341
- return JSON.stringify({ ok: true, spaces });
342
- },
343
- }),
344
-
345
- space_add_member: tool({
346
- description:
347
- "Add a user to an existing shared space with a specified role.",
348
- args: {
349
- space_id: tool.schema.string().describe("Space ID"),
350
- user_id: tool.schema.string().describe("User/tenant ID to add"),
351
- role: tool.schema.string().describe("Role: admin, member, or reader"),
352
- },
353
- async execute(args) {
354
- if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
355
- const result = await client.addSpaceMember(
356
- args.space_id,
357
- args.user_id,
358
- args.role,
359
- );
360
- if (!result) return JSON.stringify({ ok: false, error: "Failed to add member" });
361
- return JSON.stringify({ ok: true, result });
362
- },
363
- }),
364
-
365
- memory_share: tool({
366
- description:
367
- "Share a memory to a team or organization space. Creates a copy with provenance tracking.",
368
- args: {
369
- memory_id: tool.schema.string().describe("Memory ID to share"),
370
- target_space: tool.schema.string().describe("Target space ID"),
371
- },
372
- async execute(args) {
373
- if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
374
- const result = await client.shareMemory(
375
- args.memory_id,
376
- args.target_space,
377
- );
378
- if (!result) return JSON.stringify({ ok: false, error: "Failed to share memory" });
379
- return JSON.stringify({ ok: true, result });
380
- },
381
- }),
382
-
383
- memory_pull: tool({
384
- description:
385
- "Pull a shared memory from a team/organization space into your personal space.",
386
- args: {
387
- memory_id: tool.schema.string().describe("Memory ID to pull"),
388
- source_space: tool.schema.string().describe("Source space ID"),
389
- visibility: tool.schema
390
- .string()
391
- .optional()
392
- .describe("Visibility of the pulled copy"),
393
- },
394
- async execute(args) {
395
- if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
396
- const result = await client.pullMemory(
397
- args.memory_id,
398
- args.source_space,
399
- args.visibility,
400
- );
401
- if (!result) return JSON.stringify({ ok: false, error: "Failed to pull memory" });
402
- return JSON.stringify({ ok: true, result });
403
- },
404
- }),
405
-
406
- memory_reshare: tool({
407
- description:
408
- "Refresh a stale shared copy with the latest content and vector from the source memory.",
409
- args: {
410
- memory_id: tool.schema.string().describe("Shared copy memory ID to refresh"),
411
- target_space: tool.schema
412
- .string()
413
- .optional()
414
- .describe("Target space containing the copy (optional)"),
415
- },
416
- async execute(args) {
417
- if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
418
- const result = await client.reshareMemory(
419
- args.memory_id,
420
- args.target_space,
421
- );
422
- if (!result) return JSON.stringify({ ok: false, error: "Failed to reshare memory" });
423
- return JSON.stringify({ ok: true, result });
424
- },
425
- }),
426
-
427
- memory_toggle: tool({
428
- description:
429
- "Toggle Cerebro auto-store ON or OFF for current session. Does NOT affect manual memory_store calls.",
430
- args: {
431
- state: tool.schema
432
- .string()
433
- .optional()
434
- .describe("Set to 'on' or 'off'. Omit to check current status."),
435
- },
436
- async execute(args) {
437
- if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
438
- const sessionId = context.getSessionId();
439
- if (!sessionId) return JSON.stringify({ ok: false, error: "No active session" });
440
-
441
- const state = args.state?.toLowerCase();
442
- if (state === "on") {
443
- setAutoStoreEnabled(sessionId, true);
444
- return JSON.stringify({ ok: true, auto_store: true, message: "Cerebro auto-store: ON" });
445
- } else if (state === "off") {
446
- setAutoStoreEnabled(sessionId, false);
447
- return JSON.stringify({ ok: true, auto_store: false, message: "Cerebro auto-store: OFF" });
448
- } else {
449
- const current = isAutoStoreEnabled(sessionId);
450
- return JSON.stringify({ ok: true, auto_store: current, message: `Cerebro auto-store: ${current ? "ON" : "OFF"}` });
451
- }
452
- },
453
- }),
454
- };
455
- }
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import type { CerebroClient } from "./client.js";
3
+ import { type CerebroPluginConfig, resolveAgentPolicy } from "./config.js";
4
+ import { isAutoStoreEnabled, setAutoStoreEnabled } from "./index.js";
5
+
6
+ export interface ToolContext {
7
+ agentId?: string;
8
+ getSessionId: () => string | undefined;
9
+ getAgentName?: () => string;
10
+ getProjectPath?: () => string | undefined;
11
+ config?: Partial<CerebroPluginConfig>;
12
+ }
13
+
14
+ export function buildTools(client: CerebroClient, containerTags: string[], context: ToolContext) {
15
+ function checkPermission(required: "read" | "write"): boolean {
16
+ const agentId = context.getAgentName?.() || context.agentId;
17
+ if (!agentId || !context.config) return true; // no policy configured → allow
18
+ const policy = resolveAgentPolicy(agentId, context.config);
19
+ if (policy === "none") return false;
20
+ if (policy === "readonly") return required === "read";
21
+ return true; // readwrite
22
+ }
23
+
24
+ function denyMessage(required: "read" | "write"): string {
25
+ const agentId = context.getAgentName?.() || context.agentId || "unknown";
26
+ const policy = (agentId && context.config) ? resolveAgentPolicy(agentId, context.config) : "readwrite";
27
+ return `Permission denied: agent '${agentId}' has '${policy}' policy, but this operation requires '${required}' access`;
28
+ }
29
+
30
+ return {
31
+ memory_store: tool({
32
+ description:
33
+ "Store a new memory in the user's long-term memory. " +
34
+ "Use when the user explicitly asks to remember something, " +
35
+ "or when you identify important preferences, facts, or decisions worth preserving. " +
36
+ "IMPORTANT: Before calling, you MUST analyze: (1) Which category fits best? (2) Is this project-specific or cross-project? (3) Does it contain sensitive data? (4) Are tags accurate and descriptive? " +
37
+ "Every memory MUST have a correct category and at least 1 meaningful tag. " +
38
+ "Memories are automatically scoped to the current project via project_path. " +
39
+ "Set scope='global' for cross-project memories that should be visible everywhere. " +
40
+ "Private memories (visibility='private') are always agent-scoped and not bound to any project — use for sensitive data.",
41
+ args: {
42
+ content: tool.schema.string().describe(
43
+ "The information to remember. MUST be: atomic (one fact per memory), complete (self-contained without context), and precise (no ambiguity). " +
44
+ "BAD: 'fixed some bugs'. GOOD: 'Fixed memory_type validation bug in memory.rs:1480 - LLM returns illegal \"pinned\" value, added match guard to normalize to WORK/EMOTIONAL fallback'."
45
+ ),
46
+ tags: tool.schema
47
+ .array(tool.schema.string())
48
+ .optional()
49
+ .describe(
50
+ "REQUIRED. At least 1 tag in snake_case. Tags describe the memory's topic/domain for future retrieval. " +
51
+ "Examples: rust_backend, memory_system, bug_fix, user_preference, project_config. " +
52
+ "NEVER leave empty — if unsure, use a broad tag like the project name or topic area."
53
+ ),
54
+ source: tool.schema
55
+ .string()
56
+ .describe("Origin context, e.g. 'conversation', 'code-review', 'user-input', 'debugging', 'architecture-decision'"),
57
+ scope: tool.schema
58
+ .string()
59
+ .optional()
60
+ .describe(
61
+ "'project' (default) = only visible in current project context. 'global' = visible across all projects. " +
62
+ "Rule: if the memory applies generally (user preferences, general knowledge, cross-project patterns) use 'global'. " +
63
+ "If it's specific to one project's code/architecture, use 'project'."
64
+ ),
65
+ visibility: tool.schema
66
+ .string()
67
+ .optional()
68
+ .describe(
69
+ "'global' (default) = all agents can see and recall this memory. 'private' = ONLY the current agent can see it. " +
70
+ "MUST use 'private' when content contains: passwords, API keys, tokens, database credentials, SSH keys, personal information (phone, email, address), " +
71
+ "internal company details, or anything the user would NOT want other agents to access. " +
72
+ "WARNING: private memories are invisible to ALL other agents — if in doubt, ask the user. " +
73
+ "Do NOT overuse 'private' for normal work notes — default 'global' is correct for most cases."
74
+ ),
75
+ category: tool.schema
76
+ .enum(["cases", "preferences", "entities", "events", "profile", "patterns"])
77
+ .optional()
78
+ .describe(
79
+ "Memory category. MUST be one of these exact values (lowercase): " +
80
+ "'cases' (default) = work records, bug fixes, architecture decisions; " +
81
+ "'preferences' = user likes/dislikes, coding style, tool choices; " +
82
+ "'entities' = projects, tools, people, concepts; " +
83
+ "'events' = time-bound milestones (deployments, releases, incidents); " +
84
+ "'profile' = user identity traits (role, skills, team membership); " +
85
+ "'patterns' = workflows, methodologies, best practices. " +
86
+ "When in doubt, omit this field (defaults to 'cases')."
87
+ ),
88
+ },
89
+ async execute(args) {
90
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
91
+ const allTags = [...containerTags, ...(args.tags ?? [])];
92
+ const effectiveAgentId = context.getAgentName?.() || context.agentId;
93
+ const result = await client.createMemory(
94
+ args.content,
95
+ allTags,
96
+ args.source,
97
+ args.scope ?? "project",
98
+ effectiveAgentId,
99
+ context.getSessionId(),
100
+ args.visibility,
101
+ args.category,
102
+ context.getProjectPath?.(),
103
+ );
104
+ if (!result) return JSON.stringify({ ok: false, error: "The Cerebro server may be unavailable." });
105
+ return JSON.stringify({ ok: true, id: result.id, tags: result.tags });
106
+ },
107
+ }),
108
+
109
+ memory_search: tool({
110
+ description:
111
+ "Search the user's long-term memory by semantic similarity. " +
112
+ "Use to recall previously stored preferences, facts, or context. " +
113
+ "Searches are automatically filtered by the current project_path. " +
114
+ "Global-scope memories and memories without a project_path are always included in results. " +
115
+ "Private memories are visible only to the creating agent.",
116
+ args: {
117
+ query: tool.schema.string().describe("Natural-language search query"),
118
+ limit: tool.schema
119
+ .number()
120
+ .optional()
121
+ .describe("Max results to return (default 10)"),
122
+ scope: tool.schema
123
+ .string()
124
+ .optional()
125
+ .describe("Optional scope filter"),
126
+ },
127
+ async execute(args) {
128
+ if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
129
+ const results = await client.searchMemories(
130
+ args.query,
131
+ args.limit ?? 10,
132
+ args.scope,
133
+ containerTags,
134
+ context.getProjectPath?.(),
135
+ );
136
+ if (results.length === 0) return JSON.stringify({ ok: true, count: 0, results: [] });
137
+ const items = results.map((r) => ({
138
+ id: r.memory.id,
139
+ score: r.score,
140
+ content: r.memory.content.slice(0, 200),
141
+ }));
142
+ return JSON.stringify({ ok: true, count: results.length, results: items });
143
+ },
144
+ }),
145
+
146
+ memory_get: tool({
147
+ description:
148
+ "Retrieve a specific memory by its ID. " +
149
+ "Use when a recalled memory's content was truncated (e.g. medium relevance summary) " +
150
+ "and you need the full details, or when you see [rel:<id>] markers in injected context " +
151
+ "and want to fetch related memories.",
152
+ args: {
153
+ id: tool.schema.string().describe("Memory ID"),
154
+ },
155
+ async execute(args) {
156
+ if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
157
+ const memory = await client.getMemory(args.id);
158
+ if (!memory) return JSON.stringify({ ok: false, error: "not found" });
159
+ return JSON.stringify({ ok: true, memory });
160
+ },
161
+ }),
162
+
163
+ memory_update: tool({
164
+ description:
165
+ "Update the content or tags of an existing memory. " +
166
+ "Use when information needs correction or enrichment.",
167
+ args: {
168
+ id: tool.schema.string().describe("Memory ID to update"),
169
+ content: tool.schema.string().describe("New content"),
170
+ tags: tool.schema
171
+ .array(tool.schema.string())
172
+ .optional()
173
+ .describe("Replacement tags"),
174
+ },
175
+ async execute(args) {
176
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
177
+ const result = await client.updateMemory(
178
+ args.id,
179
+ args.content,
180
+ args.tags,
181
+ );
182
+ if (!result) return JSON.stringify({ ok: false, error: `Failed to update memory ${args.id}` });
183
+ return JSON.stringify({ ok: true, id: args.id });
184
+ },
185
+ }),
186
+
187
+ memory_profile: tool({
188
+ description:
189
+ "Get the user profile synthesized from stored memories. Shows preferences, patterns, and key information.",
190
+ args: {},
191
+ async execute() {
192
+ if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
193
+ const preferences = await client.getProfile();
194
+ if (preferences.length === 0) return JSON.stringify({ ok: true, count: 0, preferences: [] });
195
+ return JSON.stringify({ ok: true, count: preferences.length, preferences });
196
+ },
197
+ }),
198
+
199
+ memory_profile_stats: tool({
200
+ description:
201
+ "View user profile statistics — total preferences, slot distribution, induction run counts, etc.",
202
+ args: {},
203
+ async execute() {
204
+ if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
205
+ const stats = await client.getProfileStats();
206
+ return JSON.stringify({ ok: true, stats });
207
+ },
208
+ }),
209
+
210
+ memory_list: tool({
211
+ description:
212
+ "List the most recent memories. Use to browse what's been remembered without a search query.",
213
+ args: {
214
+ limit: tool.schema
215
+ .number()
216
+ .optional()
217
+ .describe("Max memories to return (default: 20)"),
218
+ },
219
+ async execute(args) {
220
+ if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
221
+ const memories = await client.listRecent(args.limit ?? 20);
222
+ if (memories.length === 0) return JSON.stringify({ ok: true, count: 0, memories: [] });
223
+ const items = memories.map((m) => ({
224
+ id: m.id,
225
+ content: m.content.slice(0, 120),
226
+ category: m.category,
227
+ tags: m.tags,
228
+ }));
229
+ return JSON.stringify({ ok: true, count: memories.length, memories: items });
230
+ },
231
+ }),
232
+
233
+ memory_ingest: tool({
234
+ description:
235
+ "Ingest conversation messages for intelligent extraction. The system extracts atomic facts, deduplicates, and reconciles with existing memories. " +
236
+ "Extracted memories are automatically scoped to the current project via project_path. " +
237
+ "Global-scope memories are visible across all projects.",
238
+ args: {
239
+ messages: tool.schema
240
+ .array(
241
+ tool.schema.object({
242
+ role: tool.schema.string().describe("Message role: user, assistant, or system"),
243
+ content: tool.schema.string().describe("Message content"),
244
+ }),
245
+ )
246
+ .describe("Conversation messages to ingest"),
247
+ mode: tool.schema
248
+ .enum(["smart", "raw"])
249
+ .optional()
250
+ .describe("Extraction mode: 'smart' (default) or 'raw'"),
251
+ tags: tool.schema
252
+ .array(tool.schema.string())
253
+ .optional()
254
+ .describe("Tags to apply to extracted memories"),
255
+ session_id: tool.schema
256
+ .string()
257
+ .optional()
258
+ .describe("Session ID to associate with the ingestion"),
259
+ },
260
+ async execute(args) {
261
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
262
+ const effectiveAgentId = context.getAgentName?.() || context.agentId;
263
+ const result = await client.ingestMessages(args.messages, {
264
+ mode: args.mode ?? "smart",
265
+ tags: args.tags,
266
+ sessionId: args.session_id,
267
+ agentId: effectiveAgentId,
268
+ projectPath: context.getProjectPath?.(),
269
+ });
270
+ if (result === null) return JSON.stringify({ ok: false, error: "Ingestion failed" });
271
+ return JSON.stringify({ ok: true, result });
272
+ },
273
+ }),
274
+
275
+ memory_stats: tool({
276
+ description:
277
+ "Get statistics about stored memories — counts by category, type, tier, and timeline.",
278
+ args: {},
279
+ async execute() {
280
+ if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
281
+ const stats = await client.getStats();
282
+ if (!stats) return JSON.stringify({ ok: false, error: "Failed to get stats" });
283
+ return JSON.stringify({ ok: true, stats });
284
+ },
285
+ }),
286
+
287
+ memory_delete: tool({
288
+ description:
289
+ "Delete a memory by ID. Use when the user asks to forget something.",
290
+ args: {
291
+ id: tool.schema.string().describe("Memory ID to delete"),
292
+ },
293
+ async execute(args) {
294
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
295
+ try {
296
+ await client.deleteMemory(args.id);
297
+ return JSON.stringify({ ok: true, id: args.id });
298
+ } catch {
299
+ return JSON.stringify({ ok: false, error: `Failed to delete memory ${args.id}` });
300
+ }
301
+ },
302
+ }),
303
+
304
+ space_create: tool({
305
+ description:
306
+ "Create a shared space (team or organization) for sharing memories across users and agents.",
307
+ args: {
308
+ name: tool.schema.string().describe("Name of the space"),
309
+ space_type: tool.schema
310
+ .string()
311
+ .describe("Type of space: 'team' or 'organization'"),
312
+ members: tool.schema
313
+ .array(
314
+ tool.schema.object({
315
+ user_id: tool.schema.string().describe("User/tenant ID to add"),
316
+ role: tool.schema.string().describe("Member role: admin, member, or reader"),
317
+ }),
318
+ )
319
+ .optional()
320
+ .describe("Initial members to add"),
321
+ },
322
+ async execute(args) {
323
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
324
+ const result = await client.createSpace(
325
+ args.name,
326
+ args.space_type,
327
+ args.members,
328
+ );
329
+ if (!result) return JSON.stringify({ ok: false, error: "Failed to create space" });
330
+ return JSON.stringify({ ok: true, space: result });
331
+ },
332
+ }),
333
+
334
+ space_list: tool({
335
+ description:
336
+ "List all spaces you own or are a member of.",
337
+ args: {},
338
+ async execute() {
339
+ if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
340
+ const spaces = await client.listSpaces();
341
+ return JSON.stringify({ ok: true, spaces });
342
+ },
343
+ }),
344
+
345
+ space_add_member: tool({
346
+ description:
347
+ "Add a user to an existing shared space with a specified role.",
348
+ args: {
349
+ space_id: tool.schema.string().describe("Space ID"),
350
+ user_id: tool.schema.string().describe("User/tenant ID to add"),
351
+ role: tool.schema.string().describe("Role: admin, member, or reader"),
352
+ },
353
+ async execute(args) {
354
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
355
+ const result = await client.addSpaceMember(
356
+ args.space_id,
357
+ args.user_id,
358
+ args.role,
359
+ );
360
+ if (!result) return JSON.stringify({ ok: false, error: "Failed to add member" });
361
+ return JSON.stringify({ ok: true, result });
362
+ },
363
+ }),
364
+
365
+ memory_share: tool({
366
+ description:
367
+ "Share a memory to a team or organization space. Creates a copy with provenance tracking.",
368
+ args: {
369
+ memory_id: tool.schema.string().describe("Memory ID to share"),
370
+ target_space: tool.schema.string().describe("Target space ID"),
371
+ },
372
+ async execute(args) {
373
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
374
+ const result = await client.shareMemory(
375
+ args.memory_id,
376
+ args.target_space,
377
+ );
378
+ if (!result) return JSON.stringify({ ok: false, error: "Failed to share memory" });
379
+ return JSON.stringify({ ok: true, result });
380
+ },
381
+ }),
382
+
383
+ memory_pull: tool({
384
+ description:
385
+ "Pull a shared memory from a team/organization space into your personal space.",
386
+ args: {
387
+ memory_id: tool.schema.string().describe("Memory ID to pull"),
388
+ source_space: tool.schema.string().describe("Source space ID"),
389
+ visibility: tool.schema
390
+ .string()
391
+ .optional()
392
+ .describe("Visibility of the pulled copy"),
393
+ },
394
+ async execute(args) {
395
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
396
+ const result = await client.pullMemory(
397
+ args.memory_id,
398
+ args.source_space,
399
+ args.visibility,
400
+ );
401
+ if (!result) return JSON.stringify({ ok: false, error: "Failed to pull memory" });
402
+ return JSON.stringify({ ok: true, result });
403
+ },
404
+ }),
405
+
406
+ memory_reshare: tool({
407
+ description:
408
+ "Refresh a stale shared copy with the latest content and vector from the source memory.",
409
+ args: {
410
+ memory_id: tool.schema.string().describe("Shared copy memory ID to refresh"),
411
+ target_space: tool.schema
412
+ .string()
413
+ .optional()
414
+ .describe("Target space containing the copy (optional)"),
415
+ },
416
+ async execute(args) {
417
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
418
+ const result = await client.reshareMemory(
419
+ args.memory_id,
420
+ args.target_space,
421
+ );
422
+ if (!result) return JSON.stringify({ ok: false, error: "Failed to reshare memory" });
423
+ return JSON.stringify({ ok: true, result });
424
+ },
425
+ }),
426
+
427
+ memory_toggle: tool({
428
+ description:
429
+ "Toggle Cerebro auto-store ON or OFF for current session. Does NOT affect manual memory_store calls.",
430
+ args: {
431
+ state: tool.schema
432
+ .string()
433
+ .optional()
434
+ .describe("Set to 'on' or 'off'. Omit to check current status."),
435
+ },
436
+ async execute(args) {
437
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
438
+ const sessionId = context.getSessionId();
439
+ if (!sessionId) return JSON.stringify({ ok: false, error: "No active session" });
440
+
441
+ const state = args.state?.toLowerCase();
442
+ if (state === "on") {
443
+ setAutoStoreEnabled(sessionId, true);
444
+ return JSON.stringify({ ok: true, auto_store: true, message: "Cerebro auto-store: ON" });
445
+ } else if (state === "off") {
446
+ setAutoStoreEnabled(sessionId, false);
447
+ return JSON.stringify({ ok: true, auto_store: false, message: "Cerebro auto-store: OFF" });
448
+ } else {
449
+ const current = isAutoStoreEnabled(sessionId);
450
+ return JSON.stringify({ ok: true, auto_store: current, message: `Cerebro auto-store: ${current ? "ON" : "OFF"}` });
451
+ }
452
+ },
453
+ }),
454
+ };
455
+ }