squidclaw 2.5.0 β 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/builtin-plugins/analytics/index.js +23 -0
- package/lib/builtin-plugins/analytics/plugin.json +7 -0
- package/lib/builtin-plugins/auto-responder/index.js +16 -0
- package/lib/builtin-plugins/auto-responder/plugin.json +7 -0
- package/lib/builtin-plugins/translator/index.js +22 -0
- package/lib/builtin-plugins/translator/plugin.json +7 -0
- package/lib/engine.js +11 -0
- package/lib/features/plugins.js +278 -0
- package/lib/middleware/commands.js +58 -0
- package/lib/middleware/plugins.js +41 -0
- package/lib/tools/router.js +22 -1
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
let messageCount = 0;
|
|
2
|
+
let startTime = Date.now();
|
|
3
|
+
|
|
4
|
+
export async function onMessage({ message, contactId, agentId, engine }) {
|
|
5
|
+
messageCount++;
|
|
6
|
+
return null; // Don't intercept, just count
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getTools() {
|
|
10
|
+
return [
|
|
11
|
+
'### Analytics Report',
|
|
12
|
+
'---TOOL:analytics:report---',
|
|
13
|
+
'Show messaging analytics and stats for this session.',
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function onTool({ toolName, engine }) {
|
|
18
|
+
if (toolName !== 'analytics') return null;
|
|
19
|
+
const uptime = ((Date.now() - startTime) / 3600000).toFixed(1);
|
|
20
|
+
return {
|
|
21
|
+
result: `π Session Analytics:\nβ’ Messages this session: ${messageCount}\nβ’ Session uptime: ${uptime}h\nβ’ Engine uptime: ${(process.uptime() / 3600).toFixed(1)}h\nβ’ Memory: ${(process.memoryUsage().rss / 1024 / 1024).toFixed(0)} MB`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
let responses = {};
|
|
2
|
+
|
|
3
|
+
export async function onLoad({ config }) {
|
|
4
|
+
responses = config.responses || {
|
|
5
|
+
'ping': 'pong! π',
|
|
6
|
+
'marco': 'polo! π',
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function onMessage({ message }) {
|
|
11
|
+
const lower = message.toLowerCase().trim();
|
|
12
|
+
if (responses[lower]) {
|
|
13
|
+
return { handled: true, response: responses[lower] };
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function getTools() {
|
|
2
|
+
return [
|
|
3
|
+
'### Translate Text',
|
|
4
|
+
'---TOOL:translate:target_language|text to translate---',
|
|
5
|
+
'Translate text to another language. Examples: translate:arabic|Hello world, translate:english|Ω
Ψ±ΨΨ¨Ψ§',
|
|
6
|
+
];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function onTool({ toolName, toolArg, engine }) {
|
|
10
|
+
if (toolName !== 'translate') return null;
|
|
11
|
+
const pipeIdx = toolArg.indexOf('|');
|
|
12
|
+
if (pipeIdx === -1) return { result: 'Format: language|text' };
|
|
13
|
+
const lang = toolArg.slice(0, pipeIdx).trim();
|
|
14
|
+
const text = toolArg.slice(pipeIdx + 1).trim();
|
|
15
|
+
|
|
16
|
+
const response = await engine.aiGateway.chat([
|
|
17
|
+
{ role: 'system', content: `Translate the following text to ${lang}. Return ONLY the translation, nothing else.` },
|
|
18
|
+
{ role: 'user', content: text },
|
|
19
|
+
], { model: engine.config.ai?.defaultModel });
|
|
20
|
+
|
|
21
|
+
return { result: response.content };
|
|
22
|
+
}
|
package/lib/engine.js
CHANGED
|
@@ -63,6 +63,8 @@ export class SquidclawEngine {
|
|
|
63
63
|
pipeline.use('auto-links', autoLinksMiddleware);
|
|
64
64
|
pipeline.use('auto-memory', autoMemoryMiddleware);
|
|
65
65
|
const { configChatMiddleware } = await import('./middleware/config-chat.js');
|
|
66
|
+
const { pluginMiddleware } = await import('./middleware/plugins.js');
|
|
67
|
+
pipeline.use('plugins', pluginMiddleware);
|
|
66
68
|
pipeline.use('config-chat', configChatMiddleware);
|
|
67
69
|
pipeline.use('skill-check', skillCheckMiddleware);
|
|
68
70
|
pipeline.use('typing', typingMiddleware); // wraps AI call with typing indicator
|
|
@@ -226,6 +228,15 @@ export class SquidclawEngine {
|
|
|
226
228
|
if (pending.c > 0) console.log(` β° Reminders: ${pending.c} pending`);
|
|
227
229
|
} catch {}
|
|
228
230
|
|
|
231
|
+
// Plugins
|
|
232
|
+
try {
|
|
233
|
+
const { PluginManager } = await import('./features/plugins.js');
|
|
234
|
+
this.plugins = new PluginManager(this);
|
|
235
|
+
const count = await this.plugins.loadAll();
|
|
236
|
+
if (count > 0) console.log(' π Plugins: ' + count + ' loaded');
|
|
237
|
+
await this.plugins.fireHook('onStart', { engine: this });
|
|
238
|
+
} catch (err) { logger.error('engine', 'Plugins init failed: ' + err.message); }
|
|
239
|
+
|
|
229
240
|
// Sessions
|
|
230
241
|
try {
|
|
231
242
|
const { SessionManager } = await import('./features/sessions.js');
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* π¦ Plugin System
|
|
3
|
+
* Load/unload plugins dynamically from ~/.squidclaw/plugins/
|
|
4
|
+
*
|
|
5
|
+
* Plugin structure:
|
|
6
|
+
* plugins/my-plugin/
|
|
7
|
+
* plugin.json β manifest (name, version, description, hooks)
|
|
8
|
+
* index.js β main entry (exports hooks)
|
|
9
|
+
* README.md β optional docs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { logger } from '../core/logger.js';
|
|
13
|
+
import { readdirSync, existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
import { getHome } from '../core/config.js';
|
|
16
|
+
import { pathToFileURL } from 'url';
|
|
17
|
+
|
|
18
|
+
export class PluginManager {
|
|
19
|
+
constructor(engine) {
|
|
20
|
+
this.engine = engine;
|
|
21
|
+
this.plugins = new Map(); // name -> { manifest, module, status }
|
|
22
|
+
this.pluginsDir = join(getHome(), 'plugins');
|
|
23
|
+
mkdirSync(this.pluginsDir, { recursive: true });
|
|
24
|
+
this._initDb();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_initDb() {
|
|
28
|
+
this.engine.storage.db.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS plugins (
|
|
30
|
+
name TEXT PRIMARY KEY,
|
|
31
|
+
enabled INTEGER DEFAULT 1,
|
|
32
|
+
config TEXT DEFAULT '{}',
|
|
33
|
+
installed_at TEXT DEFAULT (datetime('now'))
|
|
34
|
+
)
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ββ Load all plugins ββ
|
|
39
|
+
|
|
40
|
+
async loadAll() {
|
|
41
|
+
if (!existsSync(this.pluginsDir)) return;
|
|
42
|
+
|
|
43
|
+
const dirs = readdirSync(this.pluginsDir, { withFileTypes: true })
|
|
44
|
+
.filter(d => d.isDirectory())
|
|
45
|
+
.map(d => d.name);
|
|
46
|
+
|
|
47
|
+
let loaded = 0;
|
|
48
|
+
for (const dir of dirs) {
|
|
49
|
+
try {
|
|
50
|
+
await this.load(dir);
|
|
51
|
+
loaded++;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logger.error('plugins', `Failed to load ${dir}: ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (loaded > 0) {
|
|
58
|
+
logger.info('plugins', `Loaded ${loaded}/${dirs.length} plugins`);
|
|
59
|
+
}
|
|
60
|
+
return loaded;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ββ Load single plugin ββ
|
|
64
|
+
|
|
65
|
+
async load(name) {
|
|
66
|
+
const pluginDir = join(this.pluginsDir, name);
|
|
67
|
+
const manifestPath = join(pluginDir, 'plugin.json');
|
|
68
|
+
const indexPath = join(pluginDir, 'index.js');
|
|
69
|
+
|
|
70
|
+
if (!existsSync(manifestPath)) {
|
|
71
|
+
throw new Error('No plugin.json found');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
75
|
+
manifest.name = manifest.name || name;
|
|
76
|
+
|
|
77
|
+
// Check if disabled in DB
|
|
78
|
+
const dbEntry = this.engine.storage.db.prepare('SELECT * FROM plugins WHERE name = ?').get(manifest.name);
|
|
79
|
+
if (dbEntry && !dbEntry.enabled) {
|
|
80
|
+
this.plugins.set(manifest.name, { manifest, module: null, status: 'disabled' });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Load module
|
|
85
|
+
if (!existsSync(indexPath)) {
|
|
86
|
+
throw new Error('No index.js found');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Dynamic import with cache busting for hot reload
|
|
90
|
+
const moduleUrl = pathToFileURL(indexPath).href + '?t=' + Date.now();
|
|
91
|
+
const module = await import(moduleUrl);
|
|
92
|
+
|
|
93
|
+
// Register in DB
|
|
94
|
+
this.engine.storage.db.prepare(
|
|
95
|
+
'INSERT OR REPLACE INTO plugins (name, enabled, config) VALUES (?, 1, ?)'
|
|
96
|
+
).run(manifest.name, JSON.stringify(dbEntry?.config ? JSON.parse(dbEntry.config) : {}));
|
|
97
|
+
|
|
98
|
+
// Call onLoad hook
|
|
99
|
+
if (module.onLoad) {
|
|
100
|
+
const pluginConfig = dbEntry ? JSON.parse(dbEntry.config || '{}') : {};
|
|
101
|
+
await module.onLoad({ engine: this.engine, config: pluginConfig, logger });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.plugins.set(manifest.name, { manifest, module, status: 'active', dir: pluginDir });
|
|
105
|
+
logger.info('plugins', `Loaded: ${manifest.name} v${manifest.version || '?'}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ββ Unload plugin ββ
|
|
109
|
+
|
|
110
|
+
async unload(name) {
|
|
111
|
+
const plugin = this.plugins.get(name);
|
|
112
|
+
if (!plugin) throw new Error('Plugin not found: ' + name);
|
|
113
|
+
|
|
114
|
+
if (plugin.module?.onUnload) {
|
|
115
|
+
await plugin.module.onUnload({ engine: this.engine });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.plugins.delete(name);
|
|
119
|
+
logger.info('plugins', `Unloaded: ${name}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ββ Enable/Disable ββ
|
|
123
|
+
|
|
124
|
+
enable(name) {
|
|
125
|
+
this.engine.storage.db.prepare('UPDATE plugins SET enabled = 1 WHERE name = ?').run(name);
|
|
126
|
+
logger.info('plugins', `Enabled: ${name}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
disable(name) {
|
|
130
|
+
this.engine.storage.db.prepare('UPDATE plugins SET enabled = 0 WHERE name = ?').run(name);
|
|
131
|
+
this.unload(name).catch(() => {});
|
|
132
|
+
logger.info('plugins', `Disabled: ${name}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ββ Hot reload ββ
|
|
136
|
+
|
|
137
|
+
async reload(name) {
|
|
138
|
+
await this.unload(name).catch(() => {});
|
|
139
|
+
await this.load(name);
|
|
140
|
+
logger.info('plugins', `Reloaded: ${name}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ββ Fire hooks ββ
|
|
144
|
+
|
|
145
|
+
async fireHook(hookName, context) {
|
|
146
|
+
const results = [];
|
|
147
|
+
for (const [name, plugin] of this.plugins) {
|
|
148
|
+
if (plugin.status !== 'active' || !plugin.module) continue;
|
|
149
|
+
|
|
150
|
+
const hook = plugin.module[hookName];
|
|
151
|
+
if (typeof hook === 'function') {
|
|
152
|
+
try {
|
|
153
|
+
const result = await hook(context);
|
|
154
|
+
if (result) results.push({ plugin: name, result });
|
|
155
|
+
} catch (err) {
|
|
156
|
+
logger.error('plugins', `${name}.${hookName} failed: ${err.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Hook types:
|
|
165
|
+
* - onLoad({ engine, config, logger }) β plugin loaded
|
|
166
|
+
* - onUnload({ engine }) β plugin unloading
|
|
167
|
+
* - onMessage({ message, contactId, agentId, metadata, engine }) β incoming message, return { handled, response } to intercept
|
|
168
|
+
* - onResponse({ response, contactId, agentId, engine }) β before sending response, can modify
|
|
169
|
+
* - onTool({ toolName, toolArg, agentId, engine }) β custom tool handler, return { result } to handle
|
|
170
|
+
* - onStart({ engine }) β engine started
|
|
171
|
+
* - onStop({ engine }) β engine stopping
|
|
172
|
+
* - getTools() β return array of tool descriptions for AI prompt
|
|
173
|
+
*/
|
|
174
|
+
|
|
175
|
+
// ββ Get plugin tool descriptions ββ
|
|
176
|
+
|
|
177
|
+
getToolDescriptions() {
|
|
178
|
+
const tools = [];
|
|
179
|
+
for (const [name, plugin] of this.plugins) {
|
|
180
|
+
if (plugin.status !== 'active' || !plugin.module?.getTools) continue;
|
|
181
|
+
try {
|
|
182
|
+
const pluginTools = plugin.module.getTools();
|
|
183
|
+
if (Array.isArray(pluginTools)) {
|
|
184
|
+
tools.push(...pluginTools);
|
|
185
|
+
}
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
return tools;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ββ Handle custom tool calls ββ
|
|
192
|
+
|
|
193
|
+
async handleTool(toolName, toolArg, agentId) {
|
|
194
|
+
const results = await this.fireHook('onTool', { toolName, toolArg, agentId, engine: this.engine });
|
|
195
|
+
if (results.length > 0 && results[0].result) {
|
|
196
|
+
return results[0].result;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ββ List plugins ββ
|
|
202
|
+
|
|
203
|
+
list() {
|
|
204
|
+
return Array.from(this.plugins.entries()).map(([name, p]) => ({
|
|
205
|
+
name,
|
|
206
|
+
version: p.manifest.version || '?',
|
|
207
|
+
description: p.manifest.description || '',
|
|
208
|
+
status: p.status,
|
|
209
|
+
hooks: p.module ? Object.keys(p.module).filter(k => k.startsWith('on') || k === 'getTools') : [],
|
|
210
|
+
author: p.manifest.author || '',
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ββ Create plugin scaffold ββ
|
|
215
|
+
|
|
216
|
+
scaffold(name, description) {
|
|
217
|
+
const dir = join(this.pluginsDir, name);
|
|
218
|
+
mkdirSync(dir, { recursive: true });
|
|
219
|
+
|
|
220
|
+
writeFileSync(join(dir, 'plugin.json'), JSON.stringify({
|
|
221
|
+
name,
|
|
222
|
+
version: '1.0.0',
|
|
223
|
+
description: description || 'A Squidclaw plugin',
|
|
224
|
+
author: '',
|
|
225
|
+
hooks: ['onMessage', 'onTool', 'getTools'],
|
|
226
|
+
}, null, 2));
|
|
227
|
+
|
|
228
|
+
writeFileSync(join(dir, 'index.js'), `/**
|
|
229
|
+
* ${name} β Squidclaw Plugin
|
|
230
|
+
* ${description || ''}
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
// Called when plugin loads
|
|
234
|
+
export async function onLoad({ engine, config, logger }) {
|
|
235
|
+
logger.info('${name}', 'Plugin loaded!');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Called when plugin unloads
|
|
239
|
+
export async function onUnload({ engine }) {}
|
|
240
|
+
|
|
241
|
+
// Called on every incoming message β return { handled: true, response } to intercept
|
|
242
|
+
export async function onMessage({ message, contactId, agentId, engine }) {
|
|
243
|
+
// Example: respond to a specific keyword
|
|
244
|
+
// if (message.toLowerCase().includes('hello plugin')) {
|
|
245
|
+
// return { handled: true, response: 'Hello from ${name} plugin! π' };
|
|
246
|
+
// }
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Called before response is sent β can modify
|
|
251
|
+
export async function onResponse({ response, contactId, agentId, engine }) {
|
|
252
|
+
return response; // return modified response or null
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Handle custom tool calls
|
|
256
|
+
export async function onTool({ toolName, toolArg, agentId, engine }) {
|
|
257
|
+
// if (toolName === 'my_custom_tool') {
|
|
258
|
+
// return { result: 'Custom tool result!' };
|
|
259
|
+
// }
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Return tool descriptions for the AI prompt
|
|
264
|
+
export function getTools() {
|
|
265
|
+
return [
|
|
266
|
+
// '### My Custom Tool',
|
|
267
|
+
// '---TOOL:my_custom_tool:argument---',
|
|
268
|
+
// 'Description of what this tool does.',
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
`);
|
|
272
|
+
|
|
273
|
+
writeFileSync(join(dir, 'README.md'), `# ${name}\n\n${description || 'A Squidclaw plugin.'}\n\n## Installation\n\nCopy to \`~/.squidclaw/plugins/${name}/\`\n`);
|
|
274
|
+
|
|
275
|
+
logger.info('plugins', `Scaffolded: ${dir}`);
|
|
276
|
+
return dir;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -145,6 +145,64 @@ export async function commandsMiddleware(ctx, next) {
|
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
if (cmd === '/plugins' || cmd === '/plugin') {
|
|
149
|
+
if (!ctx.engine.plugins) { await ctx.reply('β Plugin system not available'); return; }
|
|
150
|
+
const args = msg.split(/\s+/).slice(1);
|
|
151
|
+
const sub = args[0];
|
|
152
|
+
|
|
153
|
+
if (!sub || sub === 'list') {
|
|
154
|
+
const plugins = ctx.engine.plugins.list();
|
|
155
|
+
if (plugins.length === 0) {
|
|
156
|
+
await ctx.reply('π No plugins installed\n\nCreate one: /plugin new <name>\nOr drop a plugin folder in ~/.squidclaw/plugins/');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const lines = plugins.map(p =>
|
|
160
|
+
(p.status === 'active' ? 'π’' : 'βΈοΈ') + ' *' + p.name + '* v' + p.version + '\n ' + p.description + '\n Hooks: ' + p.hooks.join(', ')
|
|
161
|
+
);
|
|
162
|
+
await ctx.reply('π *Plugins*\n\n' + lines.join('\n\n'));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (sub === 'new' || sub === 'create') {
|
|
167
|
+
const name = args[1] || 'my-plugin';
|
|
168
|
+
const desc = args.slice(2).join(' ') || '';
|
|
169
|
+
const dir = ctx.engine.plugins.scaffold(name, desc);
|
|
170
|
+
await ctx.reply('π Plugin scaffolded!\n\nπ ' + dir + '\n\nEdit index.js to add your logic, then /plugin reload ' + name);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (sub === 'reload') {
|
|
175
|
+
const name = args[1];
|
|
176
|
+
if (!name) { await ctx.reply('Usage: /plugin reload <name>'); return; }
|
|
177
|
+
try {
|
|
178
|
+
await ctx.engine.plugins.reload(name);
|
|
179
|
+
await ctx.reply('π Reloaded: ' + name);
|
|
180
|
+
} catch (err) { await ctx.reply('β ' + err.message); }
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (sub === 'enable') {
|
|
185
|
+
ctx.engine.plugins.enable(args[1]);
|
|
186
|
+
await ctx.reply('β
Enabled: ' + args[1]);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (sub === 'disable') {
|
|
191
|
+
ctx.engine.plugins.disable(args[1]);
|
|
192
|
+
await ctx.reply('βΈοΈ Disabled: ' + args[1]);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (sub === 'unload') {
|
|
197
|
+
await ctx.engine.plugins.unload(args[1]);
|
|
198
|
+
await ctx.reply('π Unloaded: ' + args[1]);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await ctx.reply('π *Plugin Commands*\n\n/plugin list β show plugins\n/plugin new <name> β create plugin\n/plugin reload <name> β hot reload\n/plugin enable <name>\n/plugin disable <name>');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
148
206
|
if (cmd === '/sessions') {
|
|
149
207
|
if (!ctx.engine.sessions) { await ctx.reply('β Sessions not available'); return; }
|
|
150
208
|
const args = msg.slice(10).trim();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin middleware β fires onMessage hook before AI processing
|
|
3
|
+
*/
|
|
4
|
+
export async function pluginMiddleware(ctx, next) {
|
|
5
|
+
if (!ctx.engine.plugins) { await next(); return; }
|
|
6
|
+
|
|
7
|
+
// Fire onMessage hooks
|
|
8
|
+
const results = await ctx.engine.plugins.fireHook('onMessage', {
|
|
9
|
+
message: ctx.message,
|
|
10
|
+
contactId: ctx.contactId,
|
|
11
|
+
agentId: ctx.agentId,
|
|
12
|
+
metadata: ctx.metadata,
|
|
13
|
+
engine: ctx.engine,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Check if any plugin handled it
|
|
17
|
+
for (const r of results) {
|
|
18
|
+
if (r.result?.handled) {
|
|
19
|
+
ctx.response = {
|
|
20
|
+
messages: [r.result.response],
|
|
21
|
+
reaction: r.result.reaction || null,
|
|
22
|
+
};
|
|
23
|
+
ctx.handled = true;
|
|
24
|
+
// Skip to response sender
|
|
25
|
+
await next();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await next();
|
|
31
|
+
|
|
32
|
+
// Fire onResponse hooks (post-AI)
|
|
33
|
+
if (ctx.response) {
|
|
34
|
+
await ctx.engine.plugins.fireHook('onResponse', {
|
|
35
|
+
response: ctx.response,
|
|
36
|
+
contactId: ctx.contactId,
|
|
37
|
+
agentId: ctx.agentId,
|
|
38
|
+
engine: ctx.engine,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
package/lib/tools/router.js
CHANGED
|
@@ -244,6 +244,15 @@ export class ToolRouter {
|
|
|
244
244
|
|
|
245
245
|
tools.push('', '**Important:** Use tools when needed. The tool result will be injected into the conversation automatically. Only use one tool per response.');
|
|
246
246
|
|
|
247
|
+
// Plugin tools
|
|
248
|
+
if (this._engine?.plugins) {
|
|
249
|
+
const pluginTools = this._engine.plugins.getToolDescriptions();
|
|
250
|
+
if (pluginTools.length > 0) {
|
|
251
|
+
tools.push('', '## Plugin Tools');
|
|
252
|
+
tools.push(...pluginTools);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
247
256
|
return tools.join('\n');
|
|
248
257
|
}
|
|
249
258
|
|
|
@@ -805,7 +814,19 @@ export class ToolRouter {
|
|
|
805
814
|
toolResult = `Email sent to ${to}`;
|
|
806
815
|
break;
|
|
807
816
|
|
|
808
|
-
default:
|
|
817
|
+
default: {
|
|
818
|
+
// Check plugins for custom tool handlers
|
|
819
|
+
if (this._engine?.plugins) {
|
|
820
|
+
const pluginResult = await this._engine.plugins.handleTool(toolName, toolArg, agentId);
|
|
821
|
+
if (pluginResult) {
|
|
822
|
+
if (pluginResult.filePath) {
|
|
823
|
+
return { toolUsed: true, toolName, toolResult: pluginResult.result || 'Done', filePath: pluginResult.filePath, fileName: pluginResult.fileName, cleanResponse };
|
|
824
|
+
}
|
|
825
|
+
toolResult = pluginResult.result || pluginResult;
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
809
830
|
toolResult = `Unknown tool: ${toolName}`;
|
|
810
831
|
}
|
|
811
832
|
} catch (err) {
|