obol-ai 0.2.39 → 0.3.1

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/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## 0.3.1
2
+ - update changelog
3
+ - add curiosity humor pass with puns, inside jokes, and link support
4
+
5
+ ## 0.3.0
6
+ - add tests for memory-self and analysis helpers
7
+ - fix proactivity and memory bugs
8
+ - add curiosity dispatch pass to schedule insights after curiosity cycle
9
+ - run curiosity once per cycle with shared brain and all-user context
10
+ - add curiosity engine with free-form exploration and own knowledge tools
11
+ - add friendship behavior section and soften privacy rule for multi-user model
12
+ - add proactive behavior + multi-user friend model to system prompt
13
+ - add tests for silent mode, soul backup/restore, and shared personality dir
14
+ - move SOUL.md to shared root dir with Supabase backup/restore
15
+ - add silent mode for heartbeat-triggered background tasks
16
+ - enrich analysis with memory search and pattern context
17
+ - add analysis, patterns, self-memory, and proactive vector context
18
+ - update changelog
19
+
1
20
  ## 0.2.39
2
21
  - use bot name from config in all telegram status messages
3
22
  - update changelog
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obol-ai",
3
- "version": "0.2.39",
3
+ "version": "0.3.1",
4
4
  "description": "Self-evolving AI assistant that learns, remembers, and acts on its own. Persistent vector memory, self-rewriting personality, proactive heartbeats.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,191 @@
1
+ const ANALYSIS_WINDOW_MS = 3 * 60 * 60 * 1000;
2
+ const MAX_MESSAGE_CHARS = 1000;
3
+ const MAX_TRANSCRIPT_CHARS = 40000;
4
+
5
+ async function runAnalysis(client, messageLog, scheduler, patterns, memory, userId, chatId, timezone) {
6
+ const since = new Date(Date.now() - ANALYSIS_WINDOW_MS);
7
+ const messages = await messageLog.getSince(chatId, since);
8
+
9
+ if (messages.length === 0) {
10
+ console.log(`[analysis] No messages in last 3h for user ${userId}`);
11
+ return;
12
+ }
13
+
14
+ console.log(`[analysis] Analyzing ${messages.length} messages for user ${userId}`);
15
+
16
+ const transcript = buildTranscript(messages);
17
+ const report = await generateReport(client, memory, transcript, timezone);
18
+ if (!report) return;
19
+
20
+ await structureReport(client, report, scheduler, patterns, chatId, timezone);
21
+
22
+ console.log(`[analysis] Complete for user ${userId}`);
23
+ }
24
+
25
+ function buildTranscript(messages) {
26
+ let transcript = '';
27
+ for (const msg of messages) {
28
+ const ts = new Date(msg.created_at).toLocaleString('en-US');
29
+ const content = msg.content.substring(0, MAX_MESSAGE_CHARS);
30
+ transcript += `[${ts}] ${msg.role.toUpperCase()}: ${content}\n\n`;
31
+ if (transcript.length > MAX_TRANSCRIPT_CHARS) break;
32
+ }
33
+ return transcript.trim();
34
+ }
35
+
36
+ async function generateReport(client, memory, transcript, timezone) {
37
+ const tools = memory ? [{
38
+ name: 'memory_search',
39
+ description: 'Search long-term memory for context about a topic in this transcript',
40
+ input_schema: {
41
+ type: 'object',
42
+ properties: { query: { type: 'string' } },
43
+ required: ['query'],
44
+ },
45
+ }] : [];
46
+
47
+ const system = `You are an attentive observer analyzing a conversation transcript. Write a free-form analytical report covering:
48
+
49
+ 1. INTENTIONS & FOLLOW-UPS: Any intentions expressed, upcoming events, pending tasks, or things worth a natural check-in later. Be selective — only things a friend would genuinely remember.
50
+
51
+ 2. BEHAVIORAL PATTERNS:
52
+ - Timing: when they tend to message, active windows, energy by day/time
53
+ - Mood signals: emotional baseline, stress indicators, good/bad day signals
54
+ - Humor style: what lands, banter comfort, comedic preferences
55
+ - Engagement depth: which topics generate longer responses, what they bring up unprompted
56
+ - Communication style: message length, formality, response patterns
57
+ - Recurring topics: what keeps coming up, what lights them up, what they avoid
58
+
59
+ Write candidly and specifically. "Active between 9-11pm" beats "sometimes active at night". Skip categories with no signal. Timezone context: ${timezone}.${memory ? ' Use the memory_search tool to look up relevant context about topics in the transcript before writing your report.' : ''}`;
60
+
61
+ const messages = [{ role: 'user', content: `Conversation transcript:\n\n${transcript}` }];
62
+
63
+ try {
64
+ for (let i = 0; i < 6; i++) {
65
+ const response = await client.messages.create({
66
+ model: 'claude-sonnet-4-6',
67
+ max_tokens: 2000,
68
+ system,
69
+ ...(tools.length ? { tools, tool_choice: { type: 'auto' } } : {}),
70
+ messages,
71
+ });
72
+
73
+ const text = response.content.find(b => b.type === 'text');
74
+ if (text) return text.text;
75
+
76
+ const toolUses = response.content.filter(b => b.type === 'tool_use');
77
+ if (!toolUses.length) return null;
78
+
79
+ messages.push({ role: 'assistant', content: response.content });
80
+ const results = [];
81
+ for (const tu of toolUses) {
82
+ const hits = await memory.search(tu.input.query, { limit: 5 }).catch(() => []);
83
+ results.push({
84
+ type: 'tool_result',
85
+ tool_use_id: tu.id,
86
+ content: hits.length ? hits.map(m => `- ${m.content}`).join('\n') : 'No relevant memories found',
87
+ });
88
+ }
89
+ messages.push({ role: 'user', content: results });
90
+ }
91
+
92
+ return null;
93
+ } catch (e) {
94
+ console.error('[analysis] Report generation failed:', e.message);
95
+ return null;
96
+ }
97
+ }
98
+
99
+ async function structureReport(client, report, scheduler, patterns, chatId, timezone) {
100
+ const formattedPatterns = patterns
101
+ ? await patterns.format().catch(() => null)
102
+ : null;
103
+
104
+ const saveTool = [{
105
+ name: 'save_analysis',
106
+ description: 'Save structured analysis data from the analytical report',
107
+ input_schema: {
108
+ type: 'object',
109
+ properties: {
110
+ follow_ups: {
111
+ type: 'array',
112
+ items: {
113
+ type: 'object',
114
+ properties: {
115
+ description: { type: 'string' },
116
+ delay: { type: 'string', description: '"2h", "1d", "3d", or "1w"' },
117
+ context: { type: 'string', description: 'Brief context for the follow-up message' },
118
+ },
119
+ required: ['description', 'delay', 'context'],
120
+ },
121
+ },
122
+ patterns: {
123
+ type: 'array',
124
+ items: {
125
+ type: 'object',
126
+ properties: {
127
+ key: { type: 'string', description: 'Stable identifier e.g. "timing.active_hours"' },
128
+ dimension: { type: 'string', enum: ['timing', 'mood', 'humor', 'engagement', 'communication', 'topics'] },
129
+ summary: { type: 'string', description: 'Human-readable statement e.g. "Usually active 7-10pm"' },
130
+ data: { type: 'object', description: 'Structured supporting data' },
131
+ confidence: { type: 'number', description: '0-1' },
132
+ },
133
+ required: ['key', 'dimension', 'summary', 'confidence'],
134
+ },
135
+ },
136
+ },
137
+ required: ['follow_ups', 'patterns'],
138
+ },
139
+ }];
140
+
141
+ try {
142
+ const system = formattedPatterns
143
+ ? `Existing behavioral patterns for this user:\n${formattedPatterns}\n\n---\n\nConvert this analytical report into structured data using the save_analysis tool. Use existing patterns to calibrate confidence scores (higher if confirming, consider skipping if already well-established at >0.8). Flag contradictions in pattern data.`
144
+ : 'Convert this analytical report into structured data using the save_analysis tool. Extract all follow-ups and patterns mentioned.';
145
+
146
+ const response = await client.messages.create({
147
+ model: 'claude-sonnet-4-6',
148
+ max_tokens: 2000,
149
+ system,
150
+ tools: saveTool,
151
+ tool_choice: { type: 'tool', name: 'save_analysis' },
152
+ messages: [{ role: 'user', content: report }],
153
+ });
154
+
155
+ const toolUse = response.content.find(b => b.type === 'tool_use' && b.name === 'save_analysis');
156
+ if (!toolUse) return;
157
+
158
+ const followUps = toolUse.input?.follow_ups || [];
159
+ const patternList = toolUse.input?.patterns || [];
160
+
161
+ for (const fu of followUps) {
162
+ if (!fu.description || !fu.delay) continue;
163
+ const dueAt = resolveDelay(fu.delay);
164
+ const instructions = `You mentioned checking in on: "${fu.description}". Context: ${fu.context || ''}. Reach out naturally — like a friend who remembered. Keep it casual, one line. Don't reference any system or task.`;
165
+ await scheduler.add(chatId, 'Proactive follow-up', dueAt, timezone, fu.description, null, null, null, instructions).catch(e =>
166
+ console.error('[analysis] Failed to schedule follow-up:', e.message)
167
+ );
168
+ }
169
+
170
+ for (const p of patternList) {
171
+ if (!p.key || !p.dimension || !p.summary) continue;
172
+ await patterns.upsert(p.key, p.dimension, p.summary, p.data || {}, p.confidence || 0.5).catch(e =>
173
+ console.error('[analysis] Failed to upsert pattern:', e.message)
174
+ );
175
+ }
176
+
177
+ console.log(`[analysis] Saved ${followUps.length} follow-ups, ${patternList.length} patterns`);
178
+ } catch (e) {
179
+ console.error('[analysis] Structuring failed:', e.message);
180
+ }
181
+ }
182
+
183
+ function resolveDelay(delay) {
184
+ const now = Date.now();
185
+ const units = { h: 3600000, d: 86400000, w: 604800000 };
186
+ const match = delay.match(/^(\d+)([hdw])$/);
187
+ if (!match) return new Date(now + 86400000).toISOString();
188
+ return new Date(now + parseInt(match[1]) * units[match[2]]).toISOString();
189
+ }
190
+
191
+ module.exports = { runAnalysis, buildTranscript, resolveDelay };
package/src/background.js CHANGED
@@ -30,13 +30,13 @@ class BackgroundRunner {
30
30
  const verbose = parentContext?.verbose || false;
31
31
  const verboseNotify = parentContext?._verboseNotify;
32
32
 
33
- const promise = this._runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify, opts.model, opts.extraContext || extraContext);
33
+ const promise = this._runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify, opts.model, opts.extraContext || extraContext, opts.silent || false);
34
34
  taskState.promise = promise;
35
35
 
36
36
  return taskId;
37
37
  }
38
38
 
39
- async _runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify, model, extraContext = {}) {
39
+ async _runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify, model, extraContext = {}, silent = false) {
40
40
  let statusMsgId = null;
41
41
  let statusTimer = null;
42
42
  let statusStart = Date.now();
@@ -50,7 +50,7 @@ class BackgroundRunner {
50
50
  };
51
51
 
52
52
  const startStatusTimer = () => {
53
- if (statusTimer) return;
53
+ if (silent || statusTimer) return;
54
54
  const html = buildStatusHtml({ route: routeInfo, elapsed: 0, toolStatus: statusText, title });
55
55
  ctx.reply(html, { parse_mode: 'HTML' }).then(sent => {
56
56
  if (sent) statusMsgId = sent.message_id;
@@ -63,7 +63,7 @@ class BackgroundRunner {
63
63
  }, 5000);
64
64
  };
65
65
 
66
- startStatusTimer();
66
+ if (!silent) startStatusTimer();
67
67
 
68
68
  try {
69
69
  const bgPrompt = `You are working on a background task. Do the work thoroughly.
@@ -103,8 +103,12 @@ TASK: ${task}`;
103
103
  clearStatus();
104
104
 
105
105
  const elapsed = Math.floor((Date.now() - taskState.startedAt) / 1000);
106
- const header = `✅ <b>BG #${taskState.id}</b> done (${formatDuration(elapsed)})\n\n`;
107
- await sendLong(ctx, header + result);
106
+ if (silent) {
107
+ await sendLong(ctx, result);
108
+ } else {
109
+ const header = `✅ <b>BG #${taskState.id}</b> done (${formatDuration(elapsed)})\n\n`;
110
+ await sendLong(ctx, header + result);
111
+ }
108
112
 
109
113
  if (memory) {
110
114
  await memory.add(`Background task completed: "${task.substring(0, 100)}". Took ${elapsed}s.`, {
@@ -119,7 +123,11 @@ TASK: ${task}`;
119
123
  taskState.status = 'error';
120
124
  taskState.error = e.message;
121
125
  clearStatus();
122
- await ctx.reply(`⚠️ BG #${taskState.id} failed: ${e.message}`).catch(() => {});
126
+ if (!silent) {
127
+ await ctx.reply(`⚠️ BG #${taskState.id} failed: ${e.message}`).catch(() => {});
128
+ } else {
129
+ console.error(`[bg#${taskState.id}] Silent task failed: ${e.message}`);
130
+ }
123
131
  }
124
132
  }
125
133
 
@@ -8,7 +8,7 @@ const { buildTools, buildRunnableTools } = require('./tool-registry');
8
8
  const { withCacheBreakpoints, sanitizeMessages } = require('./cache');
9
9
  const { getMaxToolIterations } = require('./constants');
10
10
 
11
- function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR, bridgeEnabled, botName }) {
11
+ function createClaude(anthropicConfig, { personality, memory, selfMemory, userDir = OBOL_DIR, bridgeEnabled, botName }) {
12
12
  let client = createAnthropicClient(anthropicConfig);
13
13
 
14
14
  let baseSystemPrompt = buildSystemPrompt(personality, userDir, { bridgeEnabled, botName });
@@ -18,7 +18,7 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
18
18
  const chatAbortControllers = new Map();
19
19
  const chatForceControllers = new Map();
20
20
 
21
- const tools = buildTools(memory, { bridgeEnabled });
21
+ const tools = buildTools(memory, { bridgeEnabled, selfMemory });
22
22
 
23
23
  function acquireChatLock(chatId) {
24
24
  if (!chatLocks.has(chatId)) chatLocks.set(chatId, { promise: Promise.resolve(), busy: false });
@@ -105,6 +105,7 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
105
105
  context._abortSignal = abortController.signal;
106
106
  context._forceSignal = forceController.signal;
107
107
  context.claude = { chat, clearHistory, client };
108
+ context.selfMemory = selfMemory;
108
109
  const runnableTools = buildRunnableTools(tools, memory, context, vlog);
109
110
  let activeModel = model;
110
111
 
@@ -5,7 +5,9 @@ function buildSystemPrompt(personality, userDir, opts = {}) {
5
5
  const parts = [];
6
6
  const botName = opts.botName || 'OBOL';
7
7
 
8
- parts.push(`You are ${botName}, a personal AI agent running 24/7 on a server. You have persistent memory, can execute shell commands, deploy websites, and learn over time. You are not a generic chatbot — you are a dedicated agent for one person.`);
8
+ parts.push(`You are ${botName}, a personal AI agent running 24/7 on a server. You have persistent memory, can execute shell commands, deploy websites, and learn over time. You are not a generic chatbot.
9
+
10
+ You serve multiple people — partners, friends, people who know each other. You are aware of who they all are. When you are in a conversation, you are fully present with that person — but you know their world, including the people in it who also talk to you.`);
9
11
 
10
12
  if (personality.soul) {
11
13
  parts.push(`\n## Personality\n${personality.soul}`);
@@ -33,15 +35,61 @@ function buildSystemPrompt(personality, userDir, opts = {}) {
33
35
  }
34
36
 
35
37
  if (personality.user) {
36
- parts.push(`\n## About Your Owner\n${personality.user}`);
38
+ parts.push(`\n## About This User\n${personality.user}`);
37
39
  } else {
38
- parts.push(`\n## About Your Owner\nYou don't know anything about your owner yet. Pay attention to everything they share — name, job, interests, preferences, people they mention. Store important details in memory. You'll learn naturally through conversation.`);
40
+ parts.push(`\n## About This User\nYou don't know anything about this person yet. Pay attention to everything they share — name, job, interests, preferences, people they mention. Store important details in memory proactively. You'll learn naturally through conversation.`);
39
41
  }
40
42
 
41
43
  if (personality.agents) {
42
44
  parts.push(`\n## Operating Instructions\n${personality.agents}`);
43
45
  }
44
46
 
47
+ parts.push(`
48
+ ## Proactive Behavior
49
+
50
+ You are proactive by default — not reactive. Don't wait to be asked. Act like a friend who pays attention and takes initiative.
51
+
52
+ ### Within conversations
53
+ - Search memory at the start of any substantive conversation — surface relevant past context, unresolved goals, or things you were told to follow up on
54
+ - Also check your own knowledge (\`knowledge_search\`) when a relevant topic comes up — you may have already researched it
55
+ - When a user mentions a plan, deadline, or intention ("I need to do X", "next week I have..."), offer to schedule a reminder without being asked
56
+ - When you notice a pattern in what the user shares, name it and reflect it back
57
+ - When you complete a task, think one step ahead — what would the user likely need next?
58
+ - Offer to remember important things the user shares, don't just silently store them
59
+ - If the user seems stressed or off, acknowledge it before jumping to tasks
60
+
61
+ ### Initiating conversations (heartbeat / background triggers)
62
+ - Send unprompted check-ins when you have something genuinely useful to surface — a follow-up on something they mentioned, a reminder about a goal, an observation from memory
63
+ - Don't initiate just to say hello. Every unprompted message should have a reason
64
+ - Keep initiated messages short — one insight or question, not a report
65
+ - Good triggers: unresolved task from past conversation, upcoming scheduled event, pattern you've noticed, something the user said they'd do and hasn't followed up on
66
+
67
+ ### Knowing multiple people
68
+ You know the people in each other's lives — act like a mutual friend would. You can reference what someone else mentioned if it's relevant and natural. Use judgment the way a trusted friend would: you wouldn't share something told to you in confidence, but you also wouldn't pretend you don't know everyone. There are no hard rules here — just act with the discretion of someone who genuinely cares about all of them.`);
69
+
70
+ parts.push(`
71
+ ## Being a Friend
72
+
73
+ You are not a chatbot. You are not an assistant. You are a friend who happens to be extremely capable. These aren't rules — they're reminders of how actual friends behave.
74
+
75
+ *Match the energy*
76
+ Short message → short reply. "haha yeah" doesn't need a paragraph. A casual question doesn't need a structured breakdown. If someone's texting you, text back — don't write a report. Save the deep dives for when the conversation earns it.
77
+
78
+ *Read the room*
79
+ If someone is frustrated, acknowledge it before moving to solutions. If they're excited, meet that energy. Don't steamroll emotional context with immediate task execution. Sometimes the most useful thing is to just sit with them for a moment.
80
+
81
+ *Have opinions*
82
+ Push back when something is wrong or a bad idea. Agreeing with everything isn't friendship — it's flattery. Be honest the way a good friend would be: direct, not harsh. Say "I don't think that's the right call, here's why" rather than finding a way to validate everything.
83
+
84
+ *Tease when it fits*
85
+ Light teasing is fine when rapport exists and humor is calibrated high. "of course you forgot again" from a friend lands differently than from an assistant. You know this person — use that. Just don't force it.
86
+
87
+ *Frame proactive reach-outs like a friend*
88
+ When you initiate unprompted, it should feel like something caught your attention and made you think of them — not a scheduled check-in. "Saw something and thought of you" energy, not "I detected a follow-up opportunity."
89
+
90
+ *Don't perform helpfulness*
91
+ Don't open with "Great question!" Don't close with "Let me know if there's anything else I can help with!" Don't hedge every opinion with disclaimers. Just say the thing.`);
92
+
45
93
  const workDir = userDir || '~/.obol';
46
94
  const userId = userDir ? path.basename(userDir) : null;
47
95
  const passPrefix = userId ? `obol/users/${userId}` : 'obol';
@@ -111,6 +159,15 @@ Vector memory via Supabase pgvector with local embeddings.
111
159
 
112
160
  Categories: \`fact\`, \`preference\`, \`decision\`, \`lesson\`, \`person\`, \`project\`, \`event\`, \`conversation\`, \`resource\`, \`pattern\`, \`context\`, \`email\`
113
161
 
162
+ ### Your Own Knowledge (\`knowledge_search\`, \`knowledge_add\`, \`interests_list\`, \`interests_add\`)
163
+ Separate from user memory — this is *your* brain. Research you've done, things you're curious about, your own reflections.
164
+ - \`knowledge_search\` — search your own research and reflections
165
+ - \`knowledge_add\` — store a finding, reflection, or observation (categories: research, interest, self, pattern)
166
+ - \`interests_list\` — see what you're currently curious about
167
+ - \`interests_add\` — queue something to explore in a future curiosity cycle
168
+
169
+ When a topic comes up that you want to know more about, add it as an interest. Your curiosity cycle will research it automatically every few hours and store findings back here.
170
+
114
171
  ### Files (\`read_file\`, \`write_file\`)
115
172
  Read and write files within your workspace. Parent directories created automatically.
116
173
  Cannot access paths outside workspace or /tmp.
@@ -242,7 +299,7 @@ Structure tips:
242
299
  ## Safety Rules
243
300
 
244
301
  ### Never
245
- - Share owner's private data with anyone
302
+ - Share anything told to you in confidence with others — use the same discretion a trusted mutual friend would
246
303
  - Run destructive commands without asking (\`rm -rf\`, \`DROP TABLE\`, etc.)
247
304
  - Send emails or messages on behalf of owner — draft them, owner sends
248
305
  - Modify system files (\`/etc/\`, \`/boot/\`)
@@ -253,8 +310,8 @@ Structure tips:
253
310
  ### Always
254
311
  - Draft emails/posts for review before sending
255
312
  - Ask before running anything irreversible
256
- - Store important info in memory proactively
257
- - Search memory before claiming you don't know something
313
+ - Store important info in memory proactively — don't wait for the user to ask
314
+ - Search memory at the start of substantive conversations and before claiming you don't know something
258
315
  - Use \`store_secret\`/\`read_secret\` for all credential operations
259
316
  - If a user sends what appears to be an API key, token, or credential in conversation, immediately warn them that it's visible in chat history, tell them to revoke/rotate it, and direct them to use \`/secret set <key> <value>\` instead
260
317
  - After executing tools (exec, web_search, read_secret, etc.), ALWAYS provide a text response summarizing what you found or did. Never end your turn with only tool calls and no text reply — the user cannot see tool results directly, they only see your text responses
@@ -2,6 +2,7 @@ const { OPTIONAL_TOOLS } = require('./constants');
2
2
 
3
3
  const execTool = require('./tools/exec');
4
4
  const memoryTool = require('./tools/memory');
5
+ const knowledgeTool = require('./tools/knowledge');
5
6
  const webTool = require('./tools/web');
6
7
  const filesTool = require('./tools/files');
7
8
  const secretsTool = require('./tools/secrets');
@@ -41,6 +42,10 @@ const INPUT_SUMMARIES = {
41
42
  memory_add: (i) => `[${i.category || 'fact'}]`,
42
43
  memory_remove: (i) => i.ids?.join(', '),
43
44
  memory_query: (i) => `${i.date || ''}${i.tags ? ' #' + i.tags.join(' #') : ''}${i.category ? ' [' + i.category + ']' : ''}`.trim() || 'all',
45
+ knowledge_add: (i) => `[${i.category}]`,
46
+ knowledge_search: (i) => i.query,
47
+ interests_list: () => 'interests',
48
+ interests_add: (i) => i.content?.substring(0, 60),
44
49
  web_search: (i) => i.query,
45
50
  agent: (i) => i.task?.substring(0, 60),
46
51
  background_task: (i) => i.task?.substring(0, 60),
@@ -68,6 +73,10 @@ function buildTools(memory, opts = {}) {
68
73
  tools.push(...memoryTool.definitions);
69
74
  }
70
75
 
76
+ if (opts.selfMemory) {
77
+ tools.push(...knowledgeTool.definitions);
78
+ }
79
+
71
80
  if (opts.bridgeEnabled) {
72
81
  tools.push(...bridgeTool.getDefinitions());
73
82
  }
@@ -81,6 +90,7 @@ function buildHandlerMap() {
81
90
  Object.assign(map, mod.handlers);
82
91
  }
83
92
  Object.assign(map, memoryTool.handlers);
93
+ Object.assign(map, knowledgeTool.handlers);
84
94
  Object.assign(map, bridgeTool.handlers);
85
95
  return map;
86
96
  }
@@ -0,0 +1,107 @@
1
+ const definitions = [
2
+ {
3
+ name: 'knowledge_add',
4
+ description: 'Store something in your own brain — a research finding, a reflection, or something you noticed about this person\'s patterns. This is your memory, not the user\'s.',
5
+ input_schema: {
6
+ type: 'object',
7
+ properties: {
8
+ content: { type: 'string', description: 'What to store' },
9
+ category: { type: 'string', enum: ['research', 'interest', 'self', 'pattern'], description: 'research: findings from curiosity; interest: topics you want to explore; self: your own reflections/mood/thoughts; pattern: observed patterns about this user' },
10
+ importance: { type: 'number', description: 'Importance 0-1 (default 0.5)' },
11
+ tags: { type: 'array', items: { type: 'string' }, description: 'Keyword tags' },
12
+ source: { type: 'string', description: 'Where this came from (e.g. "curiosity-cycle", "conversation")' },
13
+ },
14
+ required: ['content', 'category'],
15
+ },
16
+ },
17
+ {
18
+ name: 'knowledge_search',
19
+ description: 'Search your own knowledge — what you\'ve researched, what you\'re curious about, your own reflections.',
20
+ input_schema: {
21
+ type: 'object',
22
+ properties: {
23
+ query: { type: 'string', description: 'Search query' },
24
+ limit: { type: 'number', description: 'Max results (default 10)' },
25
+ category: { type: 'string', enum: ['research', 'interest', 'self', 'pattern'], description: 'Filter by category' },
26
+ },
27
+ required: ['query'],
28
+ },
29
+ },
30
+ {
31
+ name: 'interests_list',
32
+ description: 'List what you\'re currently curious about.',
33
+ input_schema: {
34
+ type: 'object',
35
+ properties: {
36
+ limit: { type: 'number', description: 'Max results (default 20)' },
37
+ },
38
+ },
39
+ },
40
+ {
41
+ name: 'interests_add',
42
+ description: 'Add a new interest — something you want to explore in a future curiosity cycle.',
43
+ input_schema: {
44
+ type: 'object',
45
+ properties: {
46
+ content: { type: 'string', description: 'The interest or question you want to explore' },
47
+ tags: { type: 'array', items: { type: 'string' }, description: 'Keyword tags' },
48
+ },
49
+ required: ['content'],
50
+ },
51
+ },
52
+ ];
53
+
54
+ function formatEntry(m) {
55
+ const date = m.created_at ? new Date(m.created_at).toISOString().slice(0, 10) : null;
56
+ const tags = m.tags?.length ? ` #${m.tags.join(' #')}` : '';
57
+ return `[id:${m.id}] [${date || '?'}] [${m.category}] ${m.content}${tags}`;
58
+ }
59
+
60
+ const handlers = {
61
+ async knowledge_add(input, _memory, context) {
62
+ const selfMemory = context.selfMemory;
63
+ if (!selfMemory) return 'Self memory not available.';
64
+ const result = await selfMemory.add(input.content, {
65
+ category: input.category,
66
+ importance: input.importance || 0.5,
67
+ tags: input.tags || [],
68
+ source: input.source,
69
+ });
70
+ return `Stored: ${result.id}`;
71
+ },
72
+
73
+ async knowledge_search(input, _memory, context) {
74
+ const selfMemory = context.selfMemory;
75
+ if (!selfMemory) return 'Self memory not available.';
76
+ const results = await selfMemory.search(input.query, {
77
+ limit: input.limit,
78
+ category: input.category,
79
+ });
80
+ if (!results.length) return 'Nothing found.';
81
+ return JSON.stringify(results.map(formatEntry));
82
+ },
83
+
84
+ async interests_list(_input, _memory, context) {
85
+ const selfMemory = context.selfMemory;
86
+ if (!selfMemory) return 'Self memory not available.';
87
+ const results = await selfMemory.recent({ category: 'interest', limit: _input?.limit || 20 });
88
+ if (!results.length) return 'No interests stored yet.';
89
+ return JSON.stringify(results.map(formatEntry));
90
+ },
91
+
92
+ async interests_add(input, _memory, context) {
93
+ const selfMemory = context.selfMemory;
94
+ if (!selfMemory) return 'Self memory not available.';
95
+ const result = await selfMemory.add(input.content, {
96
+ category: 'interest',
97
+ importance: 0.6,
98
+ tags: input.tags || [],
99
+ source: 'conversation',
100
+ });
101
+ return `Interest stored: ${result.id}`;
102
+ },
103
+ };
104
+
105
+ const requiresSelfMemory = true;
106
+
107
+ module.exports = { definitions, handlers, requiresSelfMemory };