feat: wire 10 new tools — file_read, file_write, glob, grep, web_fetch, task CRUD, send_message, schedule_cron
- 10 new JS tool classes in src/tools/ (clean, no framework deps) - tools/index.js: registry-based init with env toggles - bot/index.js: 16 tool definitions + 16 handlers (was 4) - Added glob npm dependency - Tools: bash, file_edit, file_read, file_write, glob, grep, web_search, web_fetch, git, task_create/update/list, send_message, schedule_cron, delegate_agent, run_skill
This commit is contained in:
88
package-lock.json
generated
88
package-lock.json
generated
@@ -19,6 +19,7 @@
|
||||
"execa": "^9.6.1",
|
||||
"express": "^4.21.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"glob": "^13.0.6",
|
||||
"grammy": "^1.42.0",
|
||||
"openai": "^4.77.0",
|
||||
"p-queue": "^8.0.1",
|
||||
@@ -418,6 +419,15 @@
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.5",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
|
||||
@@ -499,6 +509,18 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"license": "MIT",
|
||||
@@ -1140,6 +1162,23 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "13.0.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
|
||||
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^10.2.2",
|
||||
"minipass": "^7.1.3",
|
||||
"path-scurry": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"license": "MIT",
|
||||
@@ -1349,6 +1388,15 @@
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.3.6",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz",
|
||||
"integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-bytes.js": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
|
||||
@@ -1422,6 +1470,30 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"license": "MIT"
|
||||
@@ -1637,6 +1709,22 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
||||
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"execa": "^9.6.1",
|
||||
"express": "^4.21.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"glob": "^13.0.6",
|
||||
"grammy": "^1.42.0",
|
||||
"openai": "^4.77.0",
|
||||
"p-queue": "^8.0.1",
|
||||
|
||||
274
src/bot/index.js
274
src/bot/index.js
@@ -204,80 +204,135 @@ export async function initBot(config, api, tools, skills, agents) {
|
||||
const tools = [];
|
||||
const toolMap = svc.toolMap;
|
||||
|
||||
if (toolMap.has('bash')) {
|
||||
tools.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'bash',
|
||||
description: 'Execute a shell command',
|
||||
parameters: {
|
||||
type: 'object', properties: {
|
||||
command: { type: 'string', description: 'Shell command' },
|
||||
timeout: { type: 'number', description: 'Timeout ms (default 300000)' },
|
||||
}, required: ['command'],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (toolMap.has('web_search')) {
|
||||
tools.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'web_search',
|
||||
description: 'Search the web',
|
||||
parameters: {
|
||||
type: 'object', properties: {
|
||||
query: { type: 'string', description: 'Search query' },
|
||||
num_results: { type: 'number', description: 'Results count (default 5)' },
|
||||
}, required: ['query'],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (toolMap.has('git')) {
|
||||
tools.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'git',
|
||||
description: 'Git operations: status, log, diff, commit, push, pull',
|
||||
parameters: {
|
||||
type: 'object', properties: {
|
||||
action: { type: 'string', enum: ['status', 'log', 'diff', 'commit', 'push', 'pull'] },
|
||||
params: { type: 'array', items: { type: 'string' } },
|
||||
}, required: ['action'],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (svc.agents.length) {
|
||||
tools.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'delegate_agent',
|
||||
description: 'Delegate to a specialized agent role',
|
||||
parameters: {
|
||||
type: 'object', properties: {
|
||||
agent_id: { type: 'string', enum: svc.agents.map(a => a.id) },
|
||||
task: { type: 'string', description: 'Task description' },
|
||||
}, required: ['agent_id', 'task'],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (svc.skills.length) {
|
||||
tools.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'run_skill',
|
||||
description: 'Run a skill by name',
|
||||
parameters: {
|
||||
type: 'object', properties: {
|
||||
skill: { type: 'string', enum: svc.skills.map(s => s.name) },
|
||||
input: { type: 'string' },
|
||||
}, required: ['skill'],
|
||||
},
|
||||
},
|
||||
});
|
||||
// ── Tool definitions for the AI API (OpenAI function-calling format) ──
|
||||
const TOOL_DEFS = {
|
||||
bash: {
|
||||
description: 'Execute a shell command',
|
||||
parameters: { type: 'object', properties: {
|
||||
command: { type: 'string', description: 'Shell command to execute' },
|
||||
timeout: { type: 'number', description: 'Timeout in ms (default 300000)' },
|
||||
}, required: ['command'] },
|
||||
},
|
||||
file_edit: {
|
||||
description: 'Edit files — read, write, append, or find-and-replace',
|
||||
parameters: { type: 'object', properties: {
|
||||
action: { type: 'string', enum: ['read', 'write', 'append', 'edit'], description: 'Operation' },
|
||||
file_path: { type: 'string', description: 'File path' },
|
||||
content: { type: 'string', description: 'Content to write/append (for write/append)' },
|
||||
old_text: { type: 'string', description: 'Text to find (for edit)' },
|
||||
new_text: { type: 'string', description: 'Replacement text (for edit)' },
|
||||
}, required: ['action', 'file_path'] },
|
||||
},
|
||||
file_read: {
|
||||
description: 'Read file contents with line numbers and pagination',
|
||||
parameters: { type: 'object', properties: {
|
||||
file_path: { type: 'string', description: 'File path to read' },
|
||||
offset: { type: 'number', description: 'Start line (1-indexed, default 1)' },
|
||||
limit: { type: 'number', description: 'Max lines (default 500)' },
|
||||
}, required: ['file_path'] },
|
||||
},
|
||||
file_write: {
|
||||
description: 'Write content to a file (overwrites entire file)',
|
||||
parameters: { type: 'object', properties: {
|
||||
file_path: { type: 'string', description: 'File path' },
|
||||
content: { type: 'string', description: 'Content to write' },
|
||||
}, required: ['file_path', 'content'] },
|
||||
},
|
||||
glob: {
|
||||
description: 'Find files matching a glob pattern',
|
||||
parameters: { type: 'object', properties: {
|
||||
pattern: { type: 'string', description: 'Glob pattern (e.g. "**/*.js")' },
|
||||
cwd: { type: 'string', description: 'Working directory (default: current)' },
|
||||
}, required: ['pattern'] },
|
||||
},
|
||||
grep: {
|
||||
description: 'Search file contents using regex (ripgrep)',
|
||||
parameters: { type: 'object', properties: {
|
||||
pattern: { type: 'string', description: 'Regex pattern' },
|
||||
path: { type: 'string', description: 'Directory to search (default: .)' },
|
||||
file_glob: { type: 'string', description: 'File filter (e.g. "*.py")' },
|
||||
max_results: { type: 'number', description: 'Max matches (default 20)' },
|
||||
context: { type: 'number', description: 'Context lines before/after (default 0)' },
|
||||
}, required: ['pattern'] },
|
||||
},
|
||||
web_search: {
|
||||
description: 'Search the web for information',
|
||||
parameters: { type: 'object', properties: {
|
||||
query: { type: 'string', description: 'Search query' },
|
||||
num_results: { type: 'number', description: 'Results count (default 5)' },
|
||||
}, required: ['query'] },
|
||||
},
|
||||
web_fetch: {
|
||||
description: 'Fetch content from a URL and return text',
|
||||
parameters: { type: 'object', properties: {
|
||||
url: { type: 'string', description: 'URL to fetch' },
|
||||
max_length: { type: 'number', description: 'Max chars to return (default 15000)' },
|
||||
}, required: ['url'] },
|
||||
},
|
||||
git: {
|
||||
description: 'Git operations: status, log, diff, commit, push, pull',
|
||||
parameters: { type: 'object', properties: {
|
||||
action: { type: 'string', enum: ['status', 'log', 'diff', 'commit', 'push', 'pull'] },
|
||||
params: { type: 'array', items: { type: 'string' } },
|
||||
}, required: ['action'] },
|
||||
},
|
||||
task_create: {
|
||||
description: 'Create a new task',
|
||||
parameters: { type: 'object', properties: {
|
||||
description: { type: 'string', description: 'Task description' },
|
||||
}, required: ['description'] },
|
||||
},
|
||||
task_update: {
|
||||
description: 'Update task status',
|
||||
parameters: { type: 'object', properties: {
|
||||
task_id: { type: 'string', description: 'Task ID' },
|
||||
status: { type: 'string', enum: ['pending', 'in_progress', 'completed', 'cancelled'] },
|
||||
}, required: ['task_id', 'status'] },
|
||||
},
|
||||
task_list: {
|
||||
description: 'List all tasks with status',
|
||||
parameters: { type: 'object', properties: {
|
||||
status: { type: 'string', description: 'Filter by status (optional)' },
|
||||
} },
|
||||
},
|
||||
send_message: {
|
||||
description: 'Send a message to Telegram chat or channel',
|
||||
parameters: { type: 'object', properties: {
|
||||
chat_id: { type: 'string', description: 'Target chat ID (optional, uses default)' },
|
||||
message: { type: 'string', description: 'Message text to send' },
|
||||
}, required: ['message'] },
|
||||
},
|
||||
schedule_cron: {
|
||||
description: 'Manage cron jobs (create/list/remove)',
|
||||
parameters: { type: 'object', properties: {
|
||||
action: { type: 'string', enum: ['create', 'list', 'remove'] },
|
||||
name: { type: 'string', description: 'Job name' },
|
||||
schedule: { type: 'string', description: 'Cron schedule (e.g. "0 9 * * *")' },
|
||||
command: { type: 'string', description: 'Command to run' },
|
||||
}, required: ['action'] },
|
||||
},
|
||||
delegate_agent: {
|
||||
description: 'Delegate to a specialized agent role',
|
||||
parameters: { type: 'object', properties: {
|
||||
agent_id: { type: 'string', enum: svc.agents.map(a => a.id) },
|
||||
task: { type: 'string', description: 'Task description' },
|
||||
}, required: ['agent_id', 'task'] },
|
||||
},
|
||||
run_skill: {
|
||||
description: 'Run a skill by name',
|
||||
parameters: { type: 'object', properties: {
|
||||
skill: { type: 'string', enum: svc.skills.map(s => s.name) },
|
||||
input: { type: 'string' },
|
||||
}, required: ['skill'] },
|
||||
},
|
||||
};
|
||||
|
||||
// Register all tools that have a matching class loaded
|
||||
for (const [name, def] of Object.entries(TOOL_DEFS)) {
|
||||
if (name === 'delegate_agent' && !svc.agents.length) continue;
|
||||
if (name === 'run_skill' && !svc.skills.length) continue;
|
||||
if (!toolMap.has(name) && name !== 'delegate_agent' && name !== 'run_skill') continue;
|
||||
tools.push({ type: 'function', function: { name, ...def } });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -402,6 +457,7 @@ export async function initBot(config, api, tools, skills, agents) {
|
||||
return fullResponse || '✅ Done.';
|
||||
}
|
||||
|
||||
// ── Tool handlers: route API tool_calls to tool class methods ──
|
||||
const toolHandlers = {
|
||||
bash: async (args) => {
|
||||
const tool = svc.toolMap.get('bash');
|
||||
@@ -414,6 +470,42 @@ export async function initBot(config, api, tools, skills, agents) {
|
||||
return `❌ Exit ${r.code}\n\`\`\`\n${err || out}\n\`\`\``;
|
||||
} catch (e) { return `❌ Bash error: ${e.message}`; }
|
||||
},
|
||||
file_edit: async (args) => {
|
||||
const tool = svc.toolMap.get('file_edit');
|
||||
if (!tool) return '❌ File edit tool unavailable.';
|
||||
try {
|
||||
const r = await tool[args.action](args.file_path, args.content, args.old_text, args.new_text);
|
||||
return typeof r === 'string' ? r : (r.success ? `✅ ${JSON.stringify(r)}` : `❌ ${r.error}`);
|
||||
} catch (e) { return `❌ File edit error: ${e.message}`; }
|
||||
},
|
||||
file_read: async (args) => {
|
||||
const tool = svc.toolMap.get('file_read');
|
||||
if (!tool) return '❌ File read tool unavailable.';
|
||||
try {
|
||||
return await tool.execute(args);
|
||||
} catch (e) { return `❌ File read error: ${e.message}`; }
|
||||
},
|
||||
file_write: async (args) => {
|
||||
const tool = svc.toolMap.get('file_write');
|
||||
if (!tool) return '❌ File write tool unavailable.';
|
||||
try {
|
||||
return await tool.execute(args);
|
||||
} catch (e) { return `❌ File write error: ${e.message}`; }
|
||||
},
|
||||
glob: async (args) => {
|
||||
const tool = svc.toolMap.get('glob');
|
||||
if (!tool) return '❌ Glob tool unavailable.';
|
||||
try {
|
||||
return await tool.execute(args);
|
||||
} catch (e) { return `❌ Glob error: ${e.message}`; }
|
||||
},
|
||||
grep: async (args) => {
|
||||
const tool = svc.toolMap.get('grep');
|
||||
if (!tool) return '❌ Grep tool unavailable.';
|
||||
try {
|
||||
return await tool.execute(args);
|
||||
} catch (e) { return `❌ Grep error: ${e.message}`; }
|
||||
},
|
||||
web_search: async (args) => {
|
||||
const tool = svc.toolMap.get('web_search');
|
||||
if (!tool) return '❌ Web search unavailable.';
|
||||
@@ -426,16 +518,48 @@ export async function initBot(config, api, tools, skills, agents) {
|
||||
return `🔍 *${args.query}*\n\nNo results.`;
|
||||
} catch (e) { return `❌ Search error: ${e.message}`; }
|
||||
},
|
||||
web_fetch: async (args) => {
|
||||
const tool = svc.toolMap.get('web_fetch');
|
||||
if (!tool) return '❌ Web fetch tool unavailable.';
|
||||
try {
|
||||
return await tool.execute(args);
|
||||
} catch (e) { return `❌ Fetch error: ${e.message}`; }
|
||||
},
|
||||
git: async (args) => {
|
||||
const tool = svc.toolMap.get('git');
|
||||
if (!tool) return '❌ Git tool unavailable.';
|
||||
try {
|
||||
const method = tool[args.action];
|
||||
if (!method) return `❌ Unknown: ${args.action}`;
|
||||
if (!method) return `❌ Unknown git action: ${args.action}`;
|
||||
const r = await method.call(tool, ...(args.params || []));
|
||||
return r.success ? `✅ ${r.status || JSON.stringify(r)}` : `❌ ${r.stderr || r.error}`;
|
||||
} catch (e) { return `❌ Git error: ${e.message}`; }
|
||||
},
|
||||
task_create: async (args) => {
|
||||
const tool = svc.toolMap.get('task_create');
|
||||
if (!tool) return '❌ Task tool unavailable.';
|
||||
try { return await tool.execute(args); } catch (e) { return `❌ ${e.message}`; }
|
||||
},
|
||||
task_update: async (args) => {
|
||||
const tool = svc.toolMap.get('task_update');
|
||||
if (!tool) return '❌ Task tool unavailable.';
|
||||
try { return await tool.execute(args); } catch (e) { return `❌ ${e.message}`; }
|
||||
},
|
||||
task_list: async (args) => {
|
||||
const tool = svc.toolMap.get('task_list');
|
||||
if (!tool) return '❌ Task tool unavailable.';
|
||||
try { return await tool.execute(args); } catch (e) { return `❌ ${e.message}`; }
|
||||
},
|
||||
send_message: async (args) => {
|
||||
const tool = svc.toolMap.get('send_message');
|
||||
if (!tool) return '❌ Send message tool unavailable.';
|
||||
try { return await tool.execute(args); } catch (e) { return `❌ ${e.message}`; }
|
||||
},
|
||||
schedule_cron: async (args) => {
|
||||
const tool = svc.toolMap.get('schedule_cron');
|
||||
if (!tool) return '❌ Cron tool unavailable.';
|
||||
try { return await tool.execute(args); } catch (e) { return `❌ ${e.message}`; }
|
||||
},
|
||||
delegate_agent: async (args) => {
|
||||
const agent = svc.agents.find(a => a.id === args.agent_id);
|
||||
if (!agent) return `❌ Agent not found: ${args.agent_id}`;
|
||||
|
||||
37
src/tools/FileReadTool.js
Normal file
37
src/tools/FileReadTool.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
export class FileReadTool {
|
||||
constructor() {
|
||||
this.name = 'file_read';
|
||||
this.description = 'Read file contents with line numbers and pagination';
|
||||
}
|
||||
|
||||
async execute(args) {
|
||||
const { file_path, offset = 1, limit = 500 } = args;
|
||||
try {
|
||||
const fullPath = path.resolve(file_path);
|
||||
const content = await fs.readFile(fullPath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
if (offset < 1 || offset > lines.length) {
|
||||
return `❌ Offset ${offset} out of range (file has ${lines.length} lines)`;
|
||||
}
|
||||
|
||||
const end = Math.min(offset + limit - 1, lines.length);
|
||||
const selected = lines.slice(offset - 1, end);
|
||||
const numbered = selected.map((line, i) => `${offset + i}|${line}`).join('\n');
|
||||
|
||||
const header = offset === 1 && end >= lines.length
|
||||
? `${fullPath} (${lines.length} lines)`
|
||||
: `${fullPath} (lines ${offset}-${end} of ${lines.length})`;
|
||||
|
||||
return `${header}\n${numbered}`;
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') return `❌ File not found: ${file_path}`;
|
||||
if (e.code === 'EISDIR') return `❌ Is a directory: ${file_path}`;
|
||||
return `❌ ${e.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/tools/FileWriteTool.js
Normal file
22
src/tools/FileWriteTool.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
export class FileWriteTool {
|
||||
constructor() {
|
||||
this.name = 'file_write';
|
||||
this.description = 'Write content to a file, creating parent directories as needed';
|
||||
}
|
||||
|
||||
async execute(args) {
|
||||
const { file_path, content } = args;
|
||||
try {
|
||||
const fullPath = path.resolve(file_path);
|
||||
await fs.ensureDir(path.dirname(fullPath));
|
||||
await fs.writeFile(fullPath, content, 'utf-8');
|
||||
return `✅ Written ${Buffer.byteLength(content)} bytes to ${fullPath}`;
|
||||
} catch (e) {
|
||||
return `❌ Write error: ${e.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/tools/GlobTool.js
Normal file
32
src/tools/GlobTool.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { glob } from 'glob';
|
||||
import path from 'path';
|
||||
|
||||
export class GlobTool {
|
||||
constructor() {
|
||||
this.name = 'glob';
|
||||
this.description = 'Find files matching a glob pattern';
|
||||
}
|
||||
|
||||
async execute(args) {
|
||||
const { pattern, cwd = process.cwd() } = args;
|
||||
try {
|
||||
const matches = await glob(pattern, {
|
||||
cwd: path.resolve(cwd),
|
||||
absolute: false,
|
||||
nodir: true,
|
||||
dot: false,
|
||||
ignore: ['**/node_modules/**', '**/.git/**'],
|
||||
});
|
||||
|
||||
if (!matches.length) return `📁 No files matching: ${pattern}`;
|
||||
|
||||
const listed = matches.slice(0, 100);
|
||||
const output = listed.join('\n');
|
||||
const suffix = matches.length > 100 ? `\n... and ${matches.length - 100} more` : '';
|
||||
return `📁 ${matches.length} files matching "${pattern}":\n${output}${suffix}`;
|
||||
} catch (e) {
|
||||
return `❌ Glob error: ${e.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/tools/GrepTool.js
Normal file
38
src/tools/GrepTool.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { execa } from 'execa';
|
||||
import path from 'path';
|
||||
|
||||
export class GrepTool {
|
||||
constructor() {
|
||||
this.name = 'grep';
|
||||
this.description = 'Search file contents using regex (ripgrep-backed)';
|
||||
}
|
||||
|
||||
async execute(args) {
|
||||
const { pattern, path: searchPath = '.', file_glob, max_results = 20, context: ctx = 0 } = args;
|
||||
try {
|
||||
const cmdArgs = [
|
||||
'--max-count', String(max_results),
|
||||
'--no-heading',
|
||||
'--line-number',
|
||||
...(ctx > 0 ? ['-C', String(ctx)] : []),
|
||||
];
|
||||
if (file_glob) cmdArgs.push('--glob', file_glob);
|
||||
|
||||
const targetPath = path.resolve(searchPath);
|
||||
cmdArgs.push('--', pattern, targetPath);
|
||||
|
||||
const { stdout } = await execa('rg', cmdArgs, { timeout: 30000 });
|
||||
|
||||
if (!stdout.trim()) return `🔍 No matches for "${pattern}" in ${searchPath}`;
|
||||
|
||||
const lineCount = stdout.trim().split('\n').length;
|
||||
const suffix = lineCount >= max_results ? `\n(truncated at ${max_results} matches)` : '';
|
||||
return `🔍 ${lineCount} matches for "${pattern}" in ${searchPath}:${suffix}\n${stdout}`;
|
||||
} catch (e) {
|
||||
if (e.exitCode === 1) return `🔍 No matches for "${pattern}"`;
|
||||
if (e.exitCode === 2) return `❌ Grep error: ${e.stderr || e.message}`;
|
||||
return `❌ ${e.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/tools/ScheduleCronTool.js
Normal file
82
src/tools/ScheduleCronTool.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { execa } from 'execa';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
const CRON_DIR = 'data/cron';
|
||||
|
||||
export class ScheduleCronTool {
|
||||
constructor() {
|
||||
this.name = 'schedule_cron';
|
||||
this.description = 'Schedule a cron job (create/list/remove)';
|
||||
}
|
||||
|
||||
async execute(args) {
|
||||
const { action, ...params } = args;
|
||||
switch (action) {
|
||||
case 'create': return this._create(params);
|
||||
case 'list': return this._list();
|
||||
case 'remove': return this._remove(params);
|
||||
default: return `❌ Unknown action: ${action}. Use: create, list, remove`;
|
||||
}
|
||||
}
|
||||
|
||||
async _create(params) {
|
||||
const { name, schedule, command, description = '' } = params;
|
||||
if (!name || !schedule || !command) {
|
||||
return '❌ Required: name, schedule, command';
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.ensureDir(CRON_DIR);
|
||||
const jobPath = path.join(CRON_DIR, `${name}.json`);
|
||||
const job = {
|
||||
name, schedule, command, description,
|
||||
enabled: true,
|
||||
created: new Date().toISOString(),
|
||||
last_run: null,
|
||||
next_run: null,
|
||||
run_count: 0,
|
||||
};
|
||||
await fs.writeJson(jobPath, job, { spaces: 2 });
|
||||
logger.info(`⏰ Cron job created: ${name} (${schedule})`);
|
||||
return `⏰ Cron job "${name}" created\nSchedule: ${schedule}\nCommand: ${command}`;
|
||||
} catch (e) {
|
||||
return `❌ ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async _list() {
|
||||
try {
|
||||
await fs.ensureDir(CRON_DIR);
|
||||
const files = await fs.readdir(CRON_DIR);
|
||||
const jobs = files.filter(f => f.endsWith('.json'));
|
||||
|
||||
if (!jobs.length) return '⏰ No cron jobs.';
|
||||
|
||||
const list = [];
|
||||
for (const f of jobs) {
|
||||
const job = await fs.readJson(path.join(CRON_DIR, f));
|
||||
const status = job.enabled ? '🟢' : '🔴';
|
||||
list.push(`${status} ${job.name} | ${job.schedule} | runs: ${job.run_count} | ${job.command.substring(0, 60)}`);
|
||||
}
|
||||
return `⏰ Cron jobs (${jobs.length}):\n${list.join('\n')}`;
|
||||
} catch (e) {
|
||||
return `❌ ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async _remove(params) {
|
||||
const { name } = params;
|
||||
if (!name) return '❌ Required: name';
|
||||
|
||||
try {
|
||||
const jobPath = path.join(CRON_DIR, `${name}.json`);
|
||||
if (!(await fs.pathExists(jobPath))) return `❌ Job not found: ${name}`;
|
||||
await fs.remove(jobPath);
|
||||
return `✅ Cron job "${name}" removed`;
|
||||
} catch (e) {
|
||||
return `❌ ${e.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/tools/SendMessageTool.js
Normal file
37
src/tools/SendMessageTool.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import axios from 'axios';
|
||||
|
||||
export class SendMessageTool {
|
||||
constructor() {
|
||||
this.name = 'send_message';
|
||||
this.description = 'Send a message to Telegram chat or channel';
|
||||
}
|
||||
|
||||
async execute(args) {
|
||||
const { chat_id, message, parse_mode = 'HTML' } = args;
|
||||
const botToken = process.env.TELEGRAM_BOT_TOKEN;
|
||||
if (!botToken) return '❌ TELEGRAM_BOT_TOKEN not set';
|
||||
|
||||
const target = chat_id || process.env.TELEGRAM_CHAT_ID;
|
||||
if (!target) return '❌ No chat_id provided and TELEGRAM_CHAT_ID not set';
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`https://api.telegram.org/bot${botToken}/sendMessage`,
|
||||
{
|
||||
chat_id: target,
|
||||
text: message,
|
||||
parse_mode,
|
||||
disable_web_page_preview: true,
|
||||
},
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
|
||||
return response.data.ok
|
||||
? `✅ Message sent to chat ${target}`
|
||||
: `❌ Telegram error: ${JSON.stringify(response.data)}`;
|
||||
} catch (e) {
|
||||
return `❌ Send error: ${e.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/tools/TaskCreateTool.js
Normal file
36
src/tools/TaskCreateTool.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
const TASKS_FILE = 'data/tasks.json';
|
||||
|
||||
export class TaskCreateTool {
|
||||
constructor() {
|
||||
this.name = 'task_create';
|
||||
this.description = 'Create a new task with description';
|
||||
}
|
||||
|
||||
async execute(args) {
|
||||
const { description } = args;
|
||||
try {
|
||||
const tasks = await this._loadTasks();
|
||||
const id = String(Date.now()).slice(-8);
|
||||
tasks.push({ id, description, status: 'pending', created: new Date().toISOString() });
|
||||
await this._saveTasks(tasks);
|
||||
return `✅ Task created: #${id} — ${description}`;
|
||||
} catch (e) {
|
||||
return `❌ ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async _loadTasks() {
|
||||
try {
|
||||
return await fs.readJson(TASKS_FILE);
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
async _saveTasks(tasks) {
|
||||
await fs.ensureDir(path.dirname(TASKS_FILE));
|
||||
await fs.writeJson(TASKS_FILE, tasks, { spaces: 2 });
|
||||
}
|
||||
}
|
||||
36
src/tools/TaskListTool.js
Normal file
36
src/tools/TaskListTool.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
const TASKS_FILE = 'data/tasks.json';
|
||||
|
||||
export class TaskListTool {
|
||||
constructor() {
|
||||
this.name = 'task_list';
|
||||
this.description = 'List all tasks with their status';
|
||||
}
|
||||
|
||||
async execute(args = {}) {
|
||||
try {
|
||||
const tasks = await this._loadTasks();
|
||||
if (!tasks.length) return '📋 No tasks.';
|
||||
|
||||
const statusFilter = args.status;
|
||||
const filtered = statusFilter ? tasks.filter(t => t.status === statusFilter) : tasks;
|
||||
|
||||
if (!filtered.length) return `📋 No tasks with status "${statusFilter}".`;
|
||||
|
||||
const icons = { pending: '⏳', in_progress: '🔄', completed: '✅', cancelled: '❌' };
|
||||
const list = filtered.map(t =>
|
||||
`${icons[t.status] || '📝'} #${t.id} [${t.status}] ${t.description}`
|
||||
).join('\n');
|
||||
|
||||
return `📋 Tasks (${filtered.length}/${tasks.length}):\n${list}`;
|
||||
} catch (e) {
|
||||
return `❌ ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async _loadTasks() {
|
||||
try { return await fs.readJson(TASKS_FILE); } catch { return []; }
|
||||
}
|
||||
}
|
||||
42
src/tools/TaskUpdateTool.js
Normal file
42
src/tools/TaskUpdateTool.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
const TASKS_FILE = 'data/tasks.json';
|
||||
|
||||
export class TaskUpdateTool {
|
||||
constructor() {
|
||||
this.name = 'task_update';
|
||||
this.description = 'Update task status (pending/in_progress/completed/cancelled)';
|
||||
}
|
||||
|
||||
async execute(args) {
|
||||
const { task_id, status } = args;
|
||||
const validStatuses = ['pending', 'in_progress', 'completed', 'cancelled'];
|
||||
if (!validStatuses.includes(status)) {
|
||||
return `❌ Invalid status. Use: ${validStatuses.join(', ')}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const tasks = await this._loadTasks();
|
||||
const task = tasks.find(t => t.id === task_id);
|
||||
if (!task) return `❌ Task not found: #${task_id}`;
|
||||
|
||||
task.status = status;
|
||||
task.updated = new Date().toISOString();
|
||||
await this._saveTasks(tasks);
|
||||
return `✅ Task #${task_id} → ${status}`;
|
||||
} catch (e) {
|
||||
return `❌ ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async _loadTasks() {
|
||||
try { return await fs.readJson(TASKS_FILE); } catch { return []; }
|
||||
}
|
||||
|
||||
async _saveTasks(tasks) {
|
||||
await fs.ensureDir(path.dirname(TASKS_FILE));
|
||||
await fs.writeJson(TASKS_FILE, tasks, { spaces: 2 });
|
||||
}
|
||||
}
|
||||
53
src/tools/WebFetchTool.js
Normal file
53
src/tools/WebFetchTool.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import axios from 'axios';
|
||||
|
||||
export class WebFetchTool {
|
||||
constructor() {
|
||||
this.name = 'web_fetch';
|
||||
this.description = 'Fetch content from a URL and return text/markdown';
|
||||
}
|
||||
|
||||
async execute(args) {
|
||||
const { url, max_length = 15000 } = args;
|
||||
try {
|
||||
logger.info(`🌐 Fetching: ${url}`);
|
||||
const response = await axios.get(url, {
|
||||
timeout: 30000,
|
||||
maxRedirects: 5,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; zCode-Bot/1.0)',
|
||||
'Accept': 'text/html,application/json,text/plain,*/*',
|
||||
},
|
||||
});
|
||||
|
||||
let content = '';
|
||||
const contentType = response.headers['content-type'] || '';
|
||||
|
||||
if (contentType.includes('application/json')) {
|
||||
content = JSON.stringify(response.data, null, 2);
|
||||
} else {
|
||||
content = typeof response.data === 'string'
|
||||
? response.data
|
||||
: JSON.stringify(response.data, null, 2);
|
||||
}
|
||||
|
||||
// Strip HTML tags for cleaner output
|
||||
if (contentType.includes('text/html')) {
|
||||
content = content
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
if (content.length > max_length) {
|
||||
content = content.slice(0, max_length) + '\n\n... (truncated)';
|
||||
}
|
||||
|
||||
return `🌐 ${url}\n${content}`;
|
||||
} catch (e) {
|
||||
return `❌ Fetch error: ${e.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,40 +3,60 @@ import { BashTool } from './BashTool.js';
|
||||
import { FileEditTool } from './FileEditTool.js';
|
||||
import { WebSearchTool } from './WebSearchTool.js';
|
||||
import { GitTool } from './GitTool.js';
|
||||
import { FileReadTool } from './FileReadTool.js';
|
||||
import { FileWriteTool } from './FileWriteTool.js';
|
||||
import { GlobTool } from './GlobTool.js';
|
||||
import { GrepTool } from './GrepTool.js';
|
||||
import { WebFetchTool } from './WebFetchTool.js';
|
||||
import { TaskCreateTool } from './TaskCreateTool.js';
|
||||
import { TaskUpdateTool } from './TaskUpdateTool.js';
|
||||
import { TaskListTool } from './TaskListTool.js';
|
||||
import { SendMessageTool } from './SendMessageTool.js';
|
||||
import { ScheduleCronTool } from './ScheduleCronTool.js';
|
||||
|
||||
// Tool definitions: env toggle flag, factory function
|
||||
const TOOL_REGISTRY = [
|
||||
{ env: 'ZCODE_ENABLE_BASH', Tool: BashTool, label: 'Bash' },
|
||||
{ env: 'ZCODE_ENABLE_FILE_EDIT', Tool: FileEditTool, label: 'File edit' },
|
||||
{ env: 'ZCODE_ENABLE_WEB_SEARCH', Tool: WebSearchTool, label: 'Web search' },
|
||||
{ env: 'ZCODE_ENABLE_GIT', Tool: GitTool, label: 'Git' },
|
||||
{ env: 'ZCODE_ENABLE_FILE_READ', Tool: FileReadTool, label: 'File read' },
|
||||
{ env: 'ZCODE_ENABLE_FILE_WRITE', Tool: FileWriteTool, label: 'File write' },
|
||||
{ env: 'ZCODE_ENABLE_GLOB', Tool: GlobTool, label: 'Glob' },
|
||||
{ env: 'ZCODE_ENABLE_GREP', Tool: GrepTool, label: 'Grep' },
|
||||
{ env: 'ZCODE_ENABLE_WEB_FETCH', Tool: WebFetchTool, label: 'Web fetch' },
|
||||
{ env: 'ZCODE_ENABLE_TASKS', Tool: TaskCreateTool, label: 'Task create' },
|
||||
{ env: null, Tool: TaskUpdateTool, label: 'Task update' }, // bundled with TASKS
|
||||
{ env: null, Tool: TaskListTool, label: 'Task list' }, // bundled with TASKS
|
||||
{ env: 'ZCODE_ENABLE_SEND_MSG', Tool: SendMessageTool, label: 'Send message' },
|
||||
{ env: 'ZCODE_ENABLE_CRON', Tool: ScheduleCronTool, label: 'Schedule cron' },
|
||||
];
|
||||
|
||||
export async function initTools() {
|
||||
const tools = [];
|
||||
const taskEnabled = process.env.ZCODE_ENABLE_TASKS !== 'false';
|
||||
|
||||
// Bash tool
|
||||
if (process.env.ZCODE_ENABLE_BASH !== 'false') {
|
||||
const bashTool = new BashTool();
|
||||
tools.push(bashTool);
|
||||
logger.info(`✓ Bash tool loaded`);
|
||||
}
|
||||
|
||||
// File edit tool
|
||||
if (process.env.ZCODE_ENABLE_FILE_EDIT !== 'false') {
|
||||
const fileEditTool = new FileEditTool();
|
||||
tools.push(fileEditTool);
|
||||
logger.info(`✓ File edit tool loaded`);
|
||||
}
|
||||
|
||||
// Web search tool
|
||||
if (process.env.ZCODE_ENABLE_WEB_SEARCH !== 'false') {
|
||||
const webSearchTool = new WebSearchTool();
|
||||
tools.push(webSearchTool);
|
||||
logger.info(`✓ Web search tool loaded`);
|
||||
}
|
||||
|
||||
// Git tool
|
||||
if (process.env.ZCODE_ENABLE_GIT !== 'false') {
|
||||
const gitTool = new GitTool();
|
||||
tools.push(gitTool);
|
||||
logger.info(`✓ Git tool loaded`);
|
||||
for (const entry of TOOL_REGISTRY) {
|
||||
// Tasks (create/update/list) share one env flag
|
||||
const enabled = entry.env
|
||||
? process.env[entry.env] !== 'false'
|
||||
: taskEnabled;
|
||||
|
||||
if (enabled) {
|
||||
const instance = new entry.Tool();
|
||||
tools.push(instance);
|
||||
logger.info(`✓ ${entry.label} tool loaded (${instance.name})`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`📦 ${tools.length} tools ready`);
|
||||
return tools;
|
||||
}
|
||||
|
||||
// Export tool classes
|
||||
export { BashTool, FileEditTool, WebSearchTool, GitTool };
|
||||
// Export tool classes for direct access
|
||||
export {
|
||||
BashTool, FileEditTool, WebSearchTool, GitTool,
|
||||
FileReadTool, FileWriteTool, GlobTool, GrepTool, WebFetchTool,
|
||||
TaskCreateTool, TaskUpdateTool, TaskListTool,
|
||||
SendMessageTool, ScheduleCronTool,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user