neoagent 1.4.0 → 1.4.3

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 (39) hide show
  1. package/.env.example +5 -0
  2. package/com.neoagent.plist +8 -6
  3. package/docs/configuration.md +9 -1
  4. package/docs/skills.md +6 -2
  5. package/lib/manager.js +37 -10
  6. package/package.json +4 -1
  7. package/runtime/paths.js +80 -0
  8. package/server/db/database.js +78 -4
  9. package/server/index.js +5 -5
  10. package/server/public/app.html +124 -49
  11. package/server/public/assets/world-office-dark.png +0 -0
  12. package/server/public/assets/world-office-light.png +0 -0
  13. package/server/public/css/app.css +575 -242
  14. package/server/public/css/styles.css +445 -121
  15. package/server/public/js/app.js +1041 -423
  16. package/server/routes/memory.js +3 -1
  17. package/server/routes/settings.js +42 -6
  18. package/server/routes/skills.js +124 -84
  19. package/server/routes/store.js +102 -1
  20. package/server/services/ai/compaction.js +15 -31
  21. package/server/services/ai/engine.js +224 -202
  22. package/server/services/ai/history.js +188 -0
  23. package/server/services/ai/learning.js +143 -0
  24. package/server/services/ai/providers/google.js +8 -1
  25. package/server/services/ai/settings.js +80 -0
  26. package/server/services/ai/systemPrompt.js +57 -98
  27. package/server/services/ai/toolResult.js +151 -0
  28. package/server/services/ai/toolRunner.js +26 -7
  29. package/server/services/ai/toolSelector.js +140 -0
  30. package/server/services/ai/tools.js +158 -5
  31. package/server/services/browser/controller.js +124 -48
  32. package/server/services/manager.js +26 -3
  33. package/server/services/mcp/client.js +1 -1
  34. package/server/services/memory/embeddings.js +80 -14
  35. package/server/services/memory/manager.js +211 -17
  36. package/server/services/messaging/telnyx.js +3 -2
  37. package/server/services/messaging/whatsapp.js +3 -2
  38. package/server/services/scheduler/cron.js +6 -1
  39. package/server/services/websocket.js +19 -6
@@ -0,0 +1,151 @@
1
+ function clampText(text, maxChars) {
2
+ const str = String(text || '');
3
+ if (str.length <= maxChars) return str;
4
+ return `${str.slice(0, maxChars)}\n...[truncated, ${str.length} chars total]`;
5
+ }
6
+
7
+ function lineExcerpt(text, maxLines = 12, maxChars = 700) {
8
+ const str = String(text || '').trim();
9
+ if (!str) return '';
10
+ return clampText(str.split('\n').slice(0, maxLines).join('\n'), maxChars);
11
+ }
12
+
13
+ function toJsonText(value, maxChars) {
14
+ const raw = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
15
+ return clampText(raw, maxChars);
16
+ }
17
+
18
+ function trimObject(obj) {
19
+ return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined && value !== null && value !== ''));
20
+ }
21
+
22
+ function clampEnvelope(envelope, hardLimit) {
23
+ const raw = JSON.stringify(envelope);
24
+ if (raw.length <= hardLimit) return raw;
25
+
26
+ const trimmed = { ...envelope };
27
+ if (trimmed.summary) trimmed.summary = clampText(trimmed.summary, Math.max(200, hardLimit - 300));
28
+ if (trimmed.stdout) trimmed.stdout = clampText(trimmed.stdout, Math.max(160, hardLimit - 400));
29
+ if (trimmed.stderr) trimmed.stderr = clampText(trimmed.stderr, Math.max(120, hardLimit - 400));
30
+ if (trimmed.content) trimmed.content = clampText(trimmed.content, Math.max(160, hardLimit - 400));
31
+ if (trimmed.excerpt) trimmed.excerpt = clampText(trimmed.excerpt, Math.max(160, hardLimit - 400));
32
+ if (trimmed.result) trimmed.result = clampText(trimmed.result, Math.max(160, hardLimit - 400));
33
+
34
+ const fallback = JSON.stringify(trimmed);
35
+ if (fallback.length <= hardLimit) return fallback;
36
+ return clampText(fallback, hardLimit);
37
+ }
38
+
39
+ function compactToolResult(toolName, toolArgs = {}, toolResult, options = {}) {
40
+ const softLimit = Math.max(400, Math.min(Number(options.softLimit) || 1200, 2000));
41
+ const hardLimit = Math.max(softLimit, Math.min(Number(options.hardLimit) || 2000, 3000));
42
+
43
+ let envelope;
44
+
45
+ switch (toolName) {
46
+ case 'execute_command':
47
+ envelope = trimObject({
48
+ tool: toolName,
49
+ exitCode: toolResult?.exitCode,
50
+ cwd: toolResult?.cwd || toolArgs.cwd,
51
+ killed: toolResult?.killed || false,
52
+ stdout: lineExcerpt(toolResult?.stdout, 12, Math.floor(softLimit * 0.45)),
53
+ stderr: lineExcerpt(toolResult?.stderr, 8, Math.floor(softLimit * 0.25))
54
+ });
55
+ break;
56
+
57
+ case 'read_file':
58
+ envelope = trimObject({
59
+ tool: toolName,
60
+ path: toolArgs.path,
61
+ startLine: toolArgs.start_line,
62
+ endLine: toolArgs.end_line,
63
+ content: lineExcerpt(toolResult?.content || toolResult, 20, Math.floor(softLimit * 0.7))
64
+ });
65
+ break;
66
+
67
+ case 'search_files':
68
+ envelope = trimObject({
69
+ tool: toolName,
70
+ count: toolResult?.count || toolResult?.matches?.length || 0,
71
+ matches: (toolResult?.matches || []).slice(0, 6).map((match) => trimObject({
72
+ file: match.file,
73
+ line: match.line,
74
+ content: clampText(match.content, 160)
75
+ }))
76
+ });
77
+ break;
78
+
79
+ case 'browser_extract':
80
+ envelope = trimObject({
81
+ tool: toolName,
82
+ selector: toolArgs.selector || 'body',
83
+ attribute: toolArgs.attribute || 'innerText',
84
+ excerpt: lineExcerpt(toolResult?.result || toolResult?.content || toolResult, 18, Math.floor(softLimit * 0.7))
85
+ });
86
+ break;
87
+
88
+ case 'http_request':
89
+ envelope = trimObject({
90
+ tool: toolName,
91
+ status: toolResult?.status,
92
+ headers: trimObject({
93
+ contentType: toolResult?.headers?.['content-type'] || toolResult?.headers?.['Content-Type'],
94
+ contentLength: toolResult?.headers?.['content-length'] || toolResult?.headers?.['Content-Length']
95
+ }),
96
+ excerpt: lineExcerpt(toolResult?.body || toolResult, 18, Math.floor(softLimit * 0.65))
97
+ });
98
+ break;
99
+
100
+ case 'send_message':
101
+ case 'make_call':
102
+ case 'memory_save':
103
+ case 'memory_recall':
104
+ case 'memory_update_core':
105
+ case 'memory_read':
106
+ case 'memory_write':
107
+ case 'create_scheduled_task':
108
+ case 'schedule_run':
109
+ case 'list_scheduled_tasks':
110
+ case 'delete_scheduled_task':
111
+ case 'update_scheduled_task':
112
+ envelope = trimObject({
113
+ tool: toolName,
114
+ status: toolResult?.success === false || toolResult?.error ? 'error' : 'ok',
115
+ message: clampText(toolResult?.message || toolResult?.error || '', Math.floor(softLimit * 0.45)),
116
+ result: clampText(JSON.stringify(trimObject({
117
+ id: toolResult?.id,
118
+ key: toolResult?.key,
119
+ deleted: toolResult?.deleted,
120
+ sent: toolResult?.sent,
121
+ count: Array.isArray(toolResult?.results) ? toolResult.results.length : undefined
122
+ })), Math.floor(softLimit * 0.35))
123
+ });
124
+ break;
125
+
126
+ case 'spawn_subagent':
127
+ envelope = trimObject({
128
+ tool: toolName,
129
+ iterations: toolResult?.iterations,
130
+ tokens: toolResult?.tokens,
131
+ runId: toolResult?.runId,
132
+ summary: clampText(toolResult?.subagent_result || toolResult?.error || '', Math.floor(softLimit * 0.55))
133
+ });
134
+ break;
135
+
136
+ default:
137
+ envelope = trimObject({
138
+ tool: toolName,
139
+ summary: toJsonText(toolResult, Math.floor(softLimit * 0.75))
140
+ });
141
+ break;
142
+ }
143
+
144
+ return clampEnvelope(envelope, hardLimit);
145
+ }
146
+
147
+ module.exports = {
148
+ compactToolResult,
149
+ clampText,
150
+ lineExcerpt
151
+ };
@@ -1,8 +1,9 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const db = require('../../db/database');
4
+ const { AGENT_DATA_DIR } = require('../../../runtime/paths');
4
5
 
5
- const SKILLS_DIR = path.join(__dirname, '..', '..', '..', 'agent-data', 'skills');
6
+ const SKILLS_DIR = path.join(AGENT_DATA_DIR, 'skills');
6
7
 
7
8
  class SkillRunner {
8
9
  constructor() {
@@ -86,7 +87,7 @@ class SkillRunner {
86
87
  }
87
88
 
88
89
  getSkillsForPrompt() {
89
- const skills = Array.from(this.skills.values());
90
+ const skills = Array.from(this.skills.values()).filter((skill) => skill.metadata.enabled !== false);
90
91
  if (skills.length === 0) return '';
91
92
 
92
93
  let prompt = '\n## Available Skills\n';
@@ -102,7 +103,7 @@ class SkillRunner {
102
103
  getToolDefinitions() {
103
104
  const tools = [];
104
105
  for (const skill of this.skills.values()) {
105
- if (skill.metadata.tool) {
106
+ if (skill.metadata.enabled !== false && skill.metadata.tool) {
106
107
  tools.push({
107
108
  name: skill.name,
108
109
  description: skill.description,
@@ -116,6 +117,9 @@ class SkillRunner {
116
117
  async executeTool(toolName, args) {
117
118
  const skill = this.skills.get(toolName);
118
119
  if (!skill) return null;
120
+ if (skill.metadata.enabled === false) {
121
+ return { error: `Skill '${toolName}' is disabled` };
122
+ }
119
123
 
120
124
  if (skill.metadata.command) {
121
125
  const { CLIExecutor } = require('../cli/executor');
@@ -139,8 +143,10 @@ class SkillRunner {
139
143
  const filePath = path.join(skillDir, 'SKILL.md');
140
144
  fs.writeFileSync(filePath, frontmatter + `\n\n${instructions}`);
141
145
 
142
- db.prepare('INSERT OR REPLACE INTO skills (name, description, file_path, metadata, auto_created, updated_at) VALUES (?, ?, ?, ?, 1, datetime(\'now\'))')
143
- .run(safeName, description, filePath, JSON.stringify(metadata));
146
+ db.prepare(`
147
+ INSERT OR REPLACE INTO skills (name, description, file_path, metadata, enabled, auto_created, updated_at)
148
+ VALUES (?, ?, ?, ?, ?, 1, datetime('now'))
149
+ `).run(safeName, description, filePath, JSON.stringify(metadata), metadata.enabled === false ? 0 : 1);
144
150
 
145
151
  this.loadSkillFile(filePath);
146
152
 
@@ -166,12 +172,24 @@ class SkillRunner {
166
172
 
167
173
  const frontmatter = this._buildFrontmatter(name, newDesc, metaToWrite);
168
174
  fs.writeFileSync(skill.filePath, frontmatter + `\n\n${newInstructions}`);
169
- db.prepare('UPDATE skills SET description = ?, updated_at = datetime(\'now\') WHERE name = ?').run(newDesc, name);
175
+ db.prepare('UPDATE skills SET description = ?, metadata = ?, enabled = ?, updated_at = datetime(\'now\') WHERE name = ?')
176
+ .run(newDesc, JSON.stringify(metaToWrite || {}), metaToWrite?.enabled === false ? 0 : 1, name);
170
177
  this.loadSkillFile(skill.filePath);
171
178
 
172
179
  return { success: true, name, path: skill.filePath };
173
180
  }
174
181
 
182
+ getSkill(name) {
183
+ return this.skills.get(name) || null;
184
+ }
185
+
186
+ setSkillEnabled(name, enabled) {
187
+ const skill = this.skills.get(name);
188
+ if (!skill) return { error: `Skill '${name}' not found` };
189
+ const metadata = { ...skill.metadata, enabled: !!enabled };
190
+ return this.updateSkill(name, { metadata });
191
+ }
192
+
175
193
  deleteSkill(name) {
176
194
  const skill = this.skills.get(name);
177
195
  if (!skill) return { error: `Skill '${name}' not found` };
@@ -210,7 +228,8 @@ class SkillRunner {
210
228
  name: s.name,
211
229
  description: s.description,
212
230
  metadata: s.metadata,
213
- filePath: s.filePath
231
+ filePath: s.filePath,
232
+ enabled: s.metadata.enabled !== false
214
233
  }));
215
234
  }
216
235
  }
@@ -0,0 +1,140 @@
1
+ const ALWAYS_ON_TOOLS = ['notify_user'];
2
+
3
+ const PACKS = {
4
+ code: ['execute_command', 'read_file', 'list_directory', 'search_files'],
5
+ web: ['web_search', 'http_request', 'browser_navigate', 'browser_extract', 'browser_click', 'browser_type', 'browser_screenshot'],
6
+ messaging: ['send_message', 'make_call'],
7
+ memory: ['memory_recall', 'session_search', 'memory_save', 'memory_update_core', 'memory_read', 'memory_write'],
8
+ scheduling: ['create_scheduled_task', 'schedule_run', 'list_scheduled_tasks', 'update_scheduled_task', 'delete_scheduled_task'],
9
+ protocols: ['manage_protocols'],
10
+ skills: ['create_skill', 'list_skills', 'update_skill', 'delete_skill'],
11
+ images: ['generate_image', 'analyze_image'],
12
+ tables: ['generate_table', 'generate_graph'],
13
+ subagents: ['spawn_subagent'],
14
+ mcpAdmin: ['mcp_add_server', 'mcp_list_servers', 'mcp_remove_server']
15
+ };
16
+
17
+ function containsAny(text, patterns) {
18
+ return patterns.some((pattern) => pattern.test(text));
19
+ }
20
+
21
+ function detectRequestedPacks(task = '', options = {}) {
22
+ const text = String(task || '').toLowerCase();
23
+ const packs = new Set();
24
+
25
+ if (containsAny(text, [
26
+ /\b(run|execute|command|shell|terminal|bash|zsh|npm|node|python|script|repo|code|bug|fix|patch|test|build|file|folder|directory|grep|search files?)\b/,
27
+ /\b(read|open|inspect)\s+(the\s+)?(file|repo|code)\b/
28
+ ])) {
29
+ packs.add('code');
30
+ }
31
+
32
+ if (containsAny(text, [
33
+ /\b(web|website|url|page|browser|click|navigate|scrape|search|google|lookup|http|fetch|api request|screenshot)\b/,
34
+ /\bopen\b.*\bsite\b/
35
+ ])) {
36
+ packs.add('web');
37
+ }
38
+
39
+ if (containsAny(text, [
40
+ /\b(message|reply|respond|text|whatsapp|telegram|discord|dm|email|call|phone|notify|send to)\b/,
41
+ /\[no response\]/,
42
+ /\bsend_message\b/,
43
+ /\bmake_call\b/
44
+ ])) {
45
+ packs.add('messaging');
46
+ }
47
+
48
+ if (containsAny(text, [
49
+ /\bmemory\b/,
50
+ /\bremember\b/,
51
+ /\brecall\b/,
52
+ /\bprevious chat\b/,
53
+ /\blast time\b/,
54
+ /\bpast conversation\b/,
55
+ /\bpreference\b/,
56
+ /\bprofile\b/,
57
+ /\bsoul\b/
58
+ ])) {
59
+ packs.add('memory');
60
+ }
61
+
62
+ if (containsAny(text, [
63
+ /\bschedule\b/,
64
+ /\bcron\b/,
65
+ /\bremind\b/,
66
+ /\brecurring\b/,
67
+ /\bweekly\b/,
68
+ /\bdaily\b/,
69
+ /\bone-time\b/,
70
+ /\btask\b.*\blater\b/
71
+ ])) {
72
+ packs.add('scheduling');
73
+ }
74
+
75
+ if (containsAny(text, [/\bprotocol\b/, /\bplaybook\b/])) {
76
+ packs.add('protocols');
77
+ }
78
+
79
+ if (containsAny(text, [/\bskill\b/, /\binstall skill\b/, /\bcreate skill\b/])) {
80
+ packs.add('skills');
81
+ }
82
+
83
+ if (containsAny(text, [/\bimage\b/, /\bpicture\b/, /\bphoto\b/, /\bgraph\b/, /\bchart\b/, /\btable\b/, /\bqr\b/, /\bocr\b/])) {
84
+ packs.add('images');
85
+ }
86
+
87
+ if (containsAny(text, [/\btable\b/, /\bspreadsheet\b/, /\bgraph\b/, /\bchart\b/])) {
88
+ packs.add('tables');
89
+ }
90
+
91
+ if (containsAny(text, [/\bsub-?agent\b/, /\bdelegate\b/, /\bparallel\b/, /\bbackground worker\b/])) {
92
+ packs.add('subagents');
93
+ }
94
+
95
+ if (containsAny(text, [/\bmcp\b/, /\bmodel context protocol\b/, /\bserver tool\b/])) {
96
+ packs.add('mcpAdmin');
97
+ }
98
+
99
+ if (options.mediaAttachments?.length) {
100
+ packs.add('images');
101
+ }
102
+
103
+ return packs;
104
+ }
105
+
106
+ function maybeSelectMcpTools(text, mcpTools = []) {
107
+ const normalized = String(text || '').toLowerCase();
108
+ if (!normalized || !mcpTools.length) return [];
109
+
110
+ const explicitMcp = /\bmcp\b|\bmodel context protocol\b/.test(normalized);
111
+ return mcpTools.filter((tool) => {
112
+ const name = String(tool.name || '').toLowerCase();
113
+ const original = String(tool.originalName || '').toLowerCase();
114
+ const server = String(tool.serverId || '').toLowerCase();
115
+ return explicitMcp || normalized.includes(name) || normalized.includes(original) || (server && normalized.includes(server));
116
+ });
117
+ }
118
+
119
+ function selectToolsForTask(task, builtInTools = [], mcpTools = [], options = {}) {
120
+ const packs = detectRequestedPacks(task, options);
121
+ const allowNames = new Set(ALWAYS_ON_TOOLS);
122
+
123
+ for (const pack of packs) {
124
+ for (const toolName of PACKS[pack] || []) {
125
+ allowNames.add(toolName);
126
+ }
127
+ }
128
+
129
+ const selectedBuiltIns = builtInTools.filter((tool) => allowNames.has(tool.name));
130
+ const selectedMcp = maybeSelectMcpTools(task, mcpTools);
131
+
132
+ return [...selectedBuiltIns, ...selectedMcp];
133
+ }
134
+
135
+ module.exports = {
136
+ ALWAYS_ON_TOOLS,
137
+ PACKS,
138
+ detectRequestedPacks,
139
+ selectToolsForTask
140
+ };
@@ -1,13 +1,56 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const db = require('../../db/database');
4
+ const { DATA_DIR } = require('../../../runtime/paths');
5
+
6
+ function compactText(text, maxChars = 120) {
7
+ const str = String(text || '').replace(/\s+/g, ' ').trim();
8
+ if (str.length <= maxChars) return str;
9
+ const trimmed = str.slice(0, maxChars);
10
+ const sentenceBreak = Math.max(trimmed.lastIndexOf('. '), trimmed.lastIndexOf('; '), trimmed.lastIndexOf(', '));
11
+ if (sentenceBreak > 40) return trimmed.slice(0, sentenceBreak + 1).trim();
12
+ return `${trimmed.trim()}...`;
13
+ }
14
+
15
+ function compactToolDefinition(tool, options = {}) {
16
+ const compact = {
17
+ name: tool.name,
18
+ parameters: {
19
+ ...(tool.parameters || { type: 'object', properties: {} }),
20
+ properties: {}
21
+ }
22
+ };
23
+
24
+ if (options.includeDescriptions) {
25
+ compact.description = compactText(tool.description, 120);
26
+ }
27
+
28
+ if (tool.parameters?.properties) {
29
+ const properties = {};
30
+ for (const [key, value] of Object.entries(tool.parameters.properties)) {
31
+ properties[key] = { ...value };
32
+ if (options.includeDescriptions && value.description) {
33
+ properties[key].description = compactText(value.description, 70);
34
+ } else {
35
+ delete properties[key].description;
36
+ }
37
+ }
38
+ compact.parameters = {
39
+ ...compact.parameters,
40
+ properties
41
+ };
42
+ }
43
+
44
+ return compact;
45
+ }
4
46
 
5
47
  /**
6
48
  * Returns the list of available tools for the agent.
7
49
  * @param {object} app - Express app instance.
50
+ * @param {object} options - Tool filtering options.
8
51
  * @returns {Array} List of tool definitions.
9
52
  */
10
- function getAvailableTools(app) {
53
+ function getAvailableTools(app, options = {}) {
11
54
  const tools = [
12
55
  {
13
56
  name: 'execute_command',
@@ -99,6 +142,21 @@ function getAvailableTools(app) {
99
142
  required: ['script']
100
143
  }
101
144
  },
145
+ {
146
+ name: 'web_search',
147
+ description: 'Search the public web without opening the browser. Uses Brave Search API for fast result retrieval.',
148
+ parameters: {
149
+ type: 'object',
150
+ properties: {
151
+ query: { type: 'string', description: 'Search query to run' },
152
+ count: { type: 'number', description: 'Maximum number of results to return (default 5, max 10)' },
153
+ country: { type: 'string', description: 'Optional country code bias, e.g. "US", "DE", "GB"' },
154
+ search_lang: { type: 'string', description: 'Optional search language code, e.g. "en", "de"' },
155
+ freshness: { type: 'string', enum: ['pd', 'pw', 'pm', 'py'], description: 'Optional recency filter: past day, week, month, or year' }
156
+ },
157
+ required: ['query']
158
+ }
159
+ },
102
160
  {
103
161
  name: 'manage_protocols',
104
162
  description: 'Read, list, create, update, or delete text-based protocols (a pre-set list of instructions/actions). If user asks to execute a protocol, you should read it and follow its instructions.',
@@ -138,6 +196,18 @@ function getAvailableTools(app) {
138
196
  required: ['query']
139
197
  }
140
198
  },
199
+ {
200
+ name: 'session_search',
201
+ description: 'Search past runs and message threads for commands, decisions, file paths, or context from earlier conversations.',
202
+ parameters: {
203
+ type: 'object',
204
+ properties: {
205
+ query: { type: 'string', description: 'What to search for in prior sessions.' },
206
+ limit: { type: 'number', description: 'How many matching sessions to return (default 6).' }
207
+ },
208
+ required: ['query']
209
+ }
210
+ },
141
211
  {
142
212
  name: 'memory_update_core',
143
213
  description: 'Update core memory — always-injected facts that appear in every prompt. Use for critical always-relevant info: user\'s name, their main job, key standing preferences, how they want you to behave. Keep each entry concise.',
@@ -514,7 +584,12 @@ function getAvailableTools(app) {
514
584
  }
515
585
  ];
516
586
 
517
- return tools;
587
+ const compacted = tools.map((tool) => compactToolDefinition(tool, options));
588
+ if (options.names && Array.isArray(options.names)) {
589
+ const allow = new Set(options.names);
590
+ return compacted.filter((tool) => allow.has(tool.name));
591
+ }
592
+ return compacted;
518
593
  }
519
594
 
520
595
  /**
@@ -593,6 +668,74 @@ async function executeTool(toolName, args, context, engine) {
593
668
  return await controller.evaluate(args.script);
594
669
  }
595
670
 
671
+ case 'web_search': {
672
+ const apiKey = process.env.BRAVE_SEARCH_API_KEY;
673
+ if (!apiKey) return { error: 'BRAVE_SEARCH_API_KEY is not configured' };
674
+
675
+ const controller = new AbortController();
676
+ const timeoutMs = 20000;
677
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
678
+
679
+ try {
680
+ const limit = Math.max(1, Math.min(Number(args.count) || 5, 10));
681
+ const params = new URLSearchParams({
682
+ q: args.query,
683
+ count: String(limit),
684
+ text_decorations: 'false',
685
+ result_filter: 'web'
686
+ });
687
+
688
+ if (args.country) params.set('country', String(args.country).toUpperCase());
689
+ if (args.search_lang) params.set('search_lang', String(args.search_lang).toLowerCase());
690
+ if (args.freshness) params.set('freshness', args.freshness);
691
+
692
+ const res = await fetch(`https://api.search.brave.com/res/v1/web/search?${params.toString()}`, {
693
+ headers: {
694
+ Accept: 'application/json',
695
+ 'X-Subscription-Token': apiKey
696
+ },
697
+ signal: controller.signal
698
+ });
699
+
700
+ const text = await res.text();
701
+ let data = null;
702
+ try {
703
+ data = JSON.parse(text);
704
+ } catch {
705
+ data = null;
706
+ }
707
+
708
+ if (!res.ok) {
709
+ return {
710
+ error: `Brave Search API request failed with status ${res.status}`,
711
+ details: data || text.slice(0, 1000)
712
+ };
713
+ }
714
+
715
+ const rawResults = Array.isArray(data?.web?.results) ? data.web.results : [];
716
+ const results = rawResults.slice(0, limit).map((item, index) => ({
717
+ rank: index + 1,
718
+ title: item.title || '',
719
+ url: item.url || '',
720
+ description: item.description || '',
721
+ age: item.age || null,
722
+ language: item.language || null,
723
+ profile: item.profile?.long_name || item.profile?.name || null
724
+ }));
725
+
726
+ return {
727
+ query: args.query,
728
+ count: results.length,
729
+ results
730
+ };
731
+ } catch (err) {
732
+ if (err.name === 'AbortError') return { error: `Brave Search API request timed out after ${timeoutMs} ms` };
733
+ return { error: err.message };
734
+ } finally {
735
+ clearTimeout(timer);
736
+ }
737
+ }
738
+
596
739
  case 'manage_protocols': {
597
740
  try {
598
741
  if (args.action === 'list') {
@@ -641,6 +784,16 @@ async function executeTool(toolName, args, context, engine) {
641
784
  return { results };
642
785
  }
643
786
 
787
+ case 'session_search': {
788
+ const { MemoryManager } = require('../memory/manager');
789
+ const mm = new MemoryManager();
790
+ const results = mm.searchConversations(userId, args.query, {
791
+ sessions: args.limit || 6
792
+ });
793
+ if (!results.length) return { results: [], message: 'No matching sessions found' };
794
+ return { results };
795
+ }
796
+
644
797
  case 'memory_update_core': {
645
798
  const { MemoryManager } = require('../memory/manager');
646
799
  const mm = new MemoryManager();
@@ -686,13 +839,13 @@ async function executeTool(toolName, args, context, engine) {
686
839
  const end = args.end_line || lines.length;
687
840
  const sliced = lines.slice(start, end).join('\n');
688
841
  return {
689
- content: sliced.length > 50000 ? sliced.slice(0, 50000) + '\n...[truncated]' : sliced,
842
+ content: sliced.length > 20000 ? sliced.slice(0, 20000) + '\n...[truncated]' : sliced,
690
843
  totalLines: lines.length,
691
844
  rangeShown: [start + 1, Math.min(end, lines.length)]
692
845
  };
693
846
  }
694
847
  const content = fs.readFileSync(args.path, encoding);
695
- return { content: content.length > 50000 ? content.slice(0, 50000) + '\n...[truncated]' : content };
848
+ return { content: content.length > 20000 ? content.slice(0, 20000) + '\n...[truncated]' : content };
696
849
  } catch (err) {
697
850
  return { error: err.message };
698
851
  }
@@ -1000,7 +1153,7 @@ async function executeTool(toolName, args, context, engine) {
1000
1153
  n: count,
1001
1154
  response_format: 'b64_json'
1002
1155
  });
1003
- const MEDIA_DIR = path.join(__dirname, '..', '..', '..', 'data', 'media');
1156
+ const MEDIA_DIR = path.join(DATA_DIR, 'media');
1004
1157
  if (!fs.existsSync(MEDIA_DIR)) fs.mkdirSync(MEDIA_DIR, { recursive: true });
1005
1158
  const savedPaths = [];
1006
1159
  for (const img of result.data) {