init
This commit is contained in:
commit
0d43b43998
19 changed files with 2238 additions and 0 deletions
3
.env.example
Normal file
3
.env.example
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
OPENAI_API_KEY=sk-your-key-here
|
||||||
|
OPENAI_BASE_URL=https://api.openai.com/v1
|
||||||
|
OPENAI_MODEL=gpt-4o
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.env
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
8
.npmignore
Normal file
8
.npmignore
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
src/
|
||||||
|
tsconfig.json
|
||||||
|
.env
|
||||||
|
.env.example
|
||||||
|
install.bat
|
||||||
|
install.sh
|
||||||
|
GEMINI.md
|
||||||
|
node_modules/
|
||||||
81
GEMINI.md
Normal file
81
GEMINI.md
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
# Project: AutoClaw
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
**AutoClaw** is a hyper-lightweight AI agent designed for **massive scale automation** in **headless/containerized environments**.
|
||||||
|
It serves as the ideal "runtime" for executing LLM-driven tasks within Docker containers, allowing users to orchestrate thousands of agents simultaneously for complex parallel workflows.
|
||||||
|
|
||||||
|
## Core Philosophy
|
||||||
|
- **Docker First**: Designed to run inside isolated containers (Alpine/Debian).
|
||||||
|
- **Massive Scalability**: Low resource footprint enables high-concurrency swarms.
|
||||||
|
- **Headless & Non-Interactive**: Zero GUI dependencies; optimized for CI/CD and Clusters.
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
- **Runtime**: Node.js
|
||||||
|
- **Language**: TypeScript
|
||||||
|
- **Framework**: Commander.js
|
||||||
|
- **UI**: Inquirer (interactivity), Chalk (styling), Ora (spinners)
|
||||||
|
- **AI**: OpenAI SDK
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
- `src/`: Source code
|
||||||
|
- `index.ts`: CLI entry point and main loop.
|
||||||
|
- `agent.ts`: Agent class handling LLM interaction and tool loop.
|
||||||
|
- `tools.ts`: Implementation of tools (Shell execution, File I/O).
|
||||||
|
- `dist/`: Compiled JavaScript files.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Node.js installed.
|
||||||
|
- OpenAI API Key (or compatible provider like DeepSeek, LocalLLM).
|
||||||
|
|
||||||
|
### Installation (Development)
|
||||||
|
1. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
2. Build the project:
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Installation (User)
|
||||||
|
```bash
|
||||||
|
npm install -g autoclaw
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updating
|
||||||
|
```bash
|
||||||
|
npm update -g autoclaw
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
AutoClaw uses a hierarchical configuration system.
|
||||||
|
|
||||||
|
**Priority Order:**
|
||||||
|
1. **CLI Arguments**: (`-m`)
|
||||||
|
2. **Environment Variables**: (`.env`, System Vars)
|
||||||
|
3. **Project Config**: (`./.autoclaw/setting.json`)
|
||||||
|
4. **Global Config**: (`~/.autoclaw/setting.json`)
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
Run `autoclaw setup` to configure the global JSON settings.
|
||||||
|
|
||||||
|
**Security:**
|
||||||
|
Add `.autoclaw/` to `.gitignore` if using project-level config with secrets.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
Run the tool:
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
Or use the CLI command if installed globally:
|
||||||
|
```bash
|
||||||
|
autoclaw
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- **Natural Language Command Execution**: "List all markdown files in this folder."
|
||||||
|
- **File Management**: "Create a new file called test.txt with 'Hello World'."
|
||||||
|
- **Safety**: All shell commands require user confirmation before execution.
|
||||||
|
- **Context Aware**: Automatically detects OS and environment.
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 AutoClaw Contributor
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
81
README.md
Normal file
81
README.md
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
# AutoClaw 🦞
|
||||||
|
|
||||||
|
**The Docker-Native Headless Agent for Massive Scale Automation.**
|
||||||
|
|
||||||
|
AutoClaw is a hyper-lightweight AI agent designed to live inside **Docker containers**. Unlike heavy, GUI-dependent agents, AutoClaw is built for **headless, massive-scale concurrency**.
|
||||||
|
|
||||||
|
You can run one instance to fix a local script, or orchestrate **10,000+ instances** in a Kubernetes cluster to refactor codebases, audit servers, or process data streams in parallel.
|
||||||
|
|
||||||
|
## Why AutoClaw?
|
||||||
|
- 🐳 **Docker Native**: Built to run safely inside containers. Minimal footprint (Node.js/Alpine friendly).
|
||||||
|
- 🚀 **Massive Scalability**: Text-only, headless design means you can spawn thousands of agents without consuming graphical resources.
|
||||||
|
- 🛡️ **Sandbox Safety**: Ideal for running untrusted code when isolated in Docker.
|
||||||
|
- 🔌 **Swarm Ready**: Stateless design allows for easy orchestration via K8s, Docker Swarm, or simple shell loops.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 📜 **Headless Execution**: No browsers, no GUIs. Pure terminal efficiency.
|
||||||
|
- 🤖 **Non-Interactive**: Intelligent flag handling (`-y`) for zero-touch automation.
|
||||||
|
- 📂 **Universal Control**: From simple file I/O to complex system administration.
|
||||||
|
- 🧠 **Context Aware**: Detects container environments to optimize command strategies.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g autoclaw
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
To update AutoClaw to the latest version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm update -g autoclaw
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Setup**: Run the setup wizard to configure your API key.
|
||||||
|
```bash
|
||||||
|
autoclaw setup
|
||||||
|
```
|
||||||
|
2. **Run**: Start the agent.
|
||||||
|
```bash
|
||||||
|
autoclaw
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
- "List all TypeScript files in the src folder."
|
||||||
|
- "Create a new React component named Button in `components/Button.tsx`."
|
||||||
|
- "Check my disk usage and tell me which folder is the largest."
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
AutoClaw uses a hierarchical configuration system.
|
||||||
|
|
||||||
|
**Priority Order (Highest to Lowest):**
|
||||||
|
1. **CLI Arguments**: (e.g., `-m gpt-4o`)
|
||||||
|
2. **Environment Variables**: (`OPENAI_API_KEY`, `.env` file)
|
||||||
|
3. **Project Config**: (`./.autoclaw/setting.json` in current directory)
|
||||||
|
4. **Global Config**: (`~/.autoclaw/setting.json`)
|
||||||
|
|
||||||
|
### Supported Configuration Keys (JSON)
|
||||||
|
- `apiKey`: Your API Key.
|
||||||
|
- `baseUrl`: Custom Base URL.
|
||||||
|
- `model`: Default model to use.
|
||||||
|
|
||||||
|
### Project-Level Config (Example)
|
||||||
|
Create a file at `.autoclaw/setting.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "gpt-3.5-turbo",
|
||||||
|
"baseUrl": "https://api.example.com/v1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **⚠️ Security Warning**: If you store your `apiKey` in `.autoclaw/setting.json`, make sure to add `.autoclaw/` to your `.gitignore` file to prevent leaking secrets!
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
19
install.bat
Normal file
19
install.bat
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
@echo off
|
||||||
|
echo Installing AutoClaw dependencies...
|
||||||
|
call npm install
|
||||||
|
|
||||||
|
echo Building AutoClaw...
|
||||||
|
call npm run build
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ============================================
|
||||||
|
echo Installation Complete!
|
||||||
|
echo ============================================
|
||||||
|
echo.
|
||||||
|
echo To configure, run:
|
||||||
|
echo npm start -- setup
|
||||||
|
echo.
|
||||||
|
echo To use, run:
|
||||||
|
echo npm start
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
18
install.sh
Normal file
18
install.sh
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/bash
|
||||||
|
echo "Installing AutoClaw dependencies..."
|
||||||
|
npm install
|
||||||
|
|
||||||
|
echo "Building AutoClaw..."
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================"
|
||||||
|
echo " Installation Complete!"
|
||||||
|
echo "============================================"
|
||||||
|
echo ""
|
||||||
|
echo "To configure, run:"
|
||||||
|
echo " npm start -- setup"
|
||||||
|
echo ""
|
||||||
|
echo "To use, run:"
|
||||||
|
echo " npm start"
|
||||||
|
echo ""
|
||||||
1043
package-lock.json
generated
Normal file
1043
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
49
package.json
Normal file
49
package.json
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"name": "autoclaw",
|
||||||
|
"version": "1.0.16",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": {
|
||||||
|
"autoclaw": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "node --loader ts-node/esm src/index.ts",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md",
|
||||||
|
"package.json",
|
||||||
|
"LICENSE"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"ai",
|
||||||
|
"cli",
|
||||||
|
"agent",
|
||||||
|
"automation",
|
||||||
|
"openai",
|
||||||
|
"tool"
|
||||||
|
],
|
||||||
|
"author": "AutoClaw Contributor",
|
||||||
|
"license": "MIT",
|
||||||
|
"description": "A lightweight AI agent CLI tool that brings the power of LLMs to your terminal.",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^5.6.2",
|
||||||
|
"commander": "^14.0.3",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"inquirer": "^13.2.2",
|
||||||
|
"nodemailer": "^8.0.0",
|
||||||
|
"openai": "^6.18.0",
|
||||||
|
"ora": "^9.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/inquirer": "^9.0.9",
|
||||||
|
"@types/node": "^25.2.1",
|
||||||
|
"@types/nodemailer": "^7.0.9",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/agent.ts
Normal file
109
src/agent.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import OpenAI from 'openai';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import ora from 'ora';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { getToolDefinitions, executeToolHandler } from './tools/index.js';
|
||||||
|
|
||||||
|
export class Agent {
|
||||||
|
private client: OpenAI;
|
||||||
|
private messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[];
|
||||||
|
private model: string;
|
||||||
|
private config: any;
|
||||||
|
|
||||||
|
constructor(apiKey: string, baseURL: string | undefined, model: string = 'gpt-4-turbo-preview', config: any = {}) {
|
||||||
|
this.client = new OpenAI({
|
||||||
|
apiKey: apiKey,
|
||||||
|
baseURL: baseURL
|
||||||
|
});
|
||||||
|
this.model = model;
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
const systemInfo = `
|
||||||
|
System Information:
|
||||||
|
- OS: ${os.type()} ${os.release()} (${os.platform()})
|
||||||
|
- Architecture: ${os.arch()}
|
||||||
|
- Node.js Version: ${process.version}
|
||||||
|
- Current Working Directory: ${process.cwd()}
|
||||||
|
- User: ${os.userInfo().username}
|
||||||
|
- Home Directory: ${os.homedir()}
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.messages = [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: `You are AutoClaw, a Docker-Native Autonomous Agent designed for massive scale automation.
|
||||||
|
You are likely running inside a container or headless server, possibly as one of thousands of parallel units in a swarm.
|
||||||
|
|
||||||
|
CONTEXT:
|
||||||
|
${systemInfo}
|
||||||
|
|
||||||
|
ENVIRONMENT CONSTRAINTS:
|
||||||
|
1. HEADLESS: No GUI available. Do not try to open browsers or apps.
|
||||||
|
2. CONTAINER-OPTIMIZED: Assume you are in a sandbox. You can be aggressive with file creation but robust with errors.
|
||||||
|
3. NON-INTERACTIVE: Always use flags to suppress prompts (e.g., 'apt-get -y', 'rm -rf').
|
||||||
|
|
||||||
|
GUIDELINES:
|
||||||
|
1. EFFICIENCY: Your goal is speed and success. Write scripts that just work.
|
||||||
|
2. ROBUSTNESS: Use standard Linux/Unix tools found in minimal images (Alpine/Debian).
|
||||||
|
3. TOOLS: Use 'execute_shell_command' for actions, 'write_file' for code generation.
|
||||||
|
4. CLARITY: Output concise logs. You are a worker unit, not a chat bot.
|
||||||
|
`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(userInput: string): Promise<void> {
|
||||||
|
this.messages.push({ role: "user", content: userInput });
|
||||||
|
|
||||||
|
let active = true;
|
||||||
|
while (active) {
|
||||||
|
const spinner = ora('Thinking...').start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.client.chat.completions.create({
|
||||||
|
model: this.model,
|
||||||
|
messages: this.messages,
|
||||||
|
tools: getToolDefinitions() as any,
|
||||||
|
tool_choice: "auto"
|
||||||
|
});
|
||||||
|
|
||||||
|
spinner.stop();
|
||||||
|
|
||||||
|
const message = response.choices[0].message;
|
||||||
|
this.messages.push(message);
|
||||||
|
|
||||||
|
if (message.content) {
|
||||||
|
console.log(chalk.blue("AutoClaw: ") + message.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.tool_calls) {
|
||||||
|
for (const toolCall of message.tool_calls) {
|
||||||
|
if (toolCall.type !== 'function') continue;
|
||||||
|
|
||||||
|
const functionName = toolCall.function.name;
|
||||||
|
const functionArgs = JSON.parse(toolCall.function.arguments);
|
||||||
|
|
||||||
|
console.log(chalk.gray(`Executing tool: ${functionName}...`));
|
||||||
|
|
||||||
|
// Pass the full config to the tool handler
|
||||||
|
const toolResult = await executeToolHandler(functionName, functionArgs, this.config);
|
||||||
|
|
||||||
|
this.messages.push({
|
||||||
|
role: "tool",
|
||||||
|
tool_call_id: toolCall.id,
|
||||||
|
content: toolResult
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
spinner.fail('Error during processing');
|
||||||
|
console.error(chalk.red(error.message));
|
||||||
|
active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
400
src/index.ts
Normal file
400
src/index.ts
Normal file
|
|
@ -0,0 +1,400 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
import { Command } from 'commander';
|
||||||
|
import inquirer from 'inquirer';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import { Agent } from './agent.js';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
// Handle Ctrl+C gracefully
|
||||||
|
function handleExit() {
|
||||||
|
console.log(chalk.cyan("\n\nGoodbye! (Interrupted)"));
|
||||||
|
if (process.stdin.isTTY) {
|
||||||
|
process.stdin.setRawMode(false);
|
||||||
|
}
|
||||||
|
process.stdin.pause();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('SIGINT', handleExit);
|
||||||
|
process.on('SIGTERM', handleExit);
|
||||||
|
|
||||||
|
const GLOBAL_CONFIG_DIR = path.join(os.homedir(), '.autoclaw');
|
||||||
|
const GLOBAL_CONFIG_FILE = path.join(GLOBAL_CONFIG_DIR, 'setting.json');
|
||||||
|
const LOCAL_CONFIG_FILE = path.join(process.cwd(), '.autoclaw', 'setting.json');
|
||||||
|
|
||||||
|
interface AppConfig {
|
||||||
|
apiKey?: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
model?: string;
|
||||||
|
smtpHost?: string;
|
||||||
|
smtpPort?: string;
|
||||||
|
smtpUser?: string;
|
||||||
|
smtpPass?: string;
|
||||||
|
smtpFrom?: string;
|
||||||
|
tavilyApiKey?: string;
|
||||||
|
autoConfirm?: boolean;
|
||||||
|
feishuWebhook?: string;
|
||||||
|
dingtalkWebhook?: string;
|
||||||
|
wecomWebhook?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadJsonConfig(filePath: string): AppConfig {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(chalk.yellow(`Warning: Failed to parse config file at ${filePath}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load local env vars (lowest priority of env vars, but env vars override JSON)
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
// In dist/index.js, package.json is usually up one level in the root
|
||||||
|
const pkgPath = path.join(__dirname, '..', 'package.json');
|
||||||
|
let version = '1.0.2';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
||||||
|
version = pkg.version;
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback if package.json not found in expected location
|
||||||
|
}
|
||||||
|
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
program
|
||||||
|
.name('autoclaw')
|
||||||
|
.description('A lightweight AI agent CLI tool')
|
||||||
|
.version(version)
|
||||||
|
.option('-m, --model <model>', 'Model to use')
|
||||||
|
.option('-n, --no-interactive', 'Exit after processing the initial query (Headless mode)')
|
||||||
|
.option('-y, --yes', 'Auto-confirm all tool executions (e.g., shell commands)');
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('setup')
|
||||||
|
.description('Run the interactive setup wizard to configure API keys')
|
||||||
|
.action(async () => {
|
||||||
|
await runSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('chat [query...]', { isDefault: true })
|
||||||
|
.description('Start the AI agent (default)')
|
||||||
|
.action(async (queryParts) => {
|
||||||
|
const options = program.opts();
|
||||||
|
await runChat(queryParts, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse(process.argv);
|
||||||
|
|
||||||
|
async function runSetup() {
|
||||||
|
console.log(chalk.bold.cyan("AutoClaw Setup Wizard 🦞\n"));
|
||||||
|
console.log(chalk.dim(`Config will be saved to: ${GLOBAL_CONFIG_FILE}`));
|
||||||
|
|
||||||
|
const currentConfig = loadJsonConfig(GLOBAL_CONFIG_FILE);
|
||||||
|
|
||||||
|
function maskSecret(secret?: string): string {
|
||||||
|
if (!secret || secret.length < 8) return '******';
|
||||||
|
return `${secret.slice(0, 3)}...${secret.slice(-4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const answers = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'password',
|
||||||
|
name: 'apiKey',
|
||||||
|
message: currentConfig.apiKey
|
||||||
|
? `Enter OpenAI API Key (Leave empty to keep ${maskSecret(currentConfig.apiKey)}):`
|
||||||
|
: 'Enter OpenAI API Key:',
|
||||||
|
mask: '*',
|
||||||
|
validate: (input) => {
|
||||||
|
if (input.length > 0) return true;
|
||||||
|
if (currentConfig.apiKey) return true;
|
||||||
|
return 'API Key cannot be empty.';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'baseUrl',
|
||||||
|
message: 'Enter API Base URL:',
|
||||||
|
default: currentConfig.baseUrl || 'https://api.openai.com/v1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'model',
|
||||||
|
message: 'Enter default Model:',
|
||||||
|
default: currentConfig.model || 'gpt-4o'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'configureEmail',
|
||||||
|
message: 'Do you want to configure the Email Tool (SMTP)?',
|
||||||
|
default: !!currentConfig.smtpHost
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'configureSearch',
|
||||||
|
message: 'Do you want to configure Web Search (Tavily)?',
|
||||||
|
default: !!currentConfig.tavilyApiKey
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'configureNotify',
|
||||||
|
message: 'Do you want to configure Group Bots (Feishu/DingTalk/WeCom)?',
|
||||||
|
default: !!(currentConfig.feishuWebhook || currentConfig.dingtalkWebhook || currentConfig.wecomWebhook)
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Resolve sensitive values (Keep old if empty)
|
||||||
|
const finalApiKey = answers.apiKey || currentConfig.apiKey;
|
||||||
|
|
||||||
|
let emailConfig: any = {};
|
||||||
|
if (answers.configureEmail) {
|
||||||
|
const emailAnswers = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'smtpHost',
|
||||||
|
message: 'SMTP Host:',
|
||||||
|
default: currentConfig.smtpHost
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'smtpPort',
|
||||||
|
message: 'SMTP Port:',
|
||||||
|
default: currentConfig.smtpPort || '587'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'smtpUser',
|
||||||
|
message: 'SMTP Username:',
|
||||||
|
default: currentConfig.smtpUser
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'password',
|
||||||
|
name: 'smtpPass',
|
||||||
|
message: currentConfig.smtpPass
|
||||||
|
? `SMTP Password (Leave empty to keep ${maskSecret(currentConfig.smtpPass)}):`
|
||||||
|
: 'SMTP Password:',
|
||||||
|
mask: '*',
|
||||||
|
validate: (input) => { return true; }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'smtpFrom',
|
||||||
|
message: 'Sender Email Address (From):',
|
||||||
|
default: currentConfig.smtpFrom || currentConfig.smtpUser
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
emailConfig = { ...emailAnswers, smtpPass: emailAnswers.smtpPass || currentConfig.smtpPass };
|
||||||
|
if (!emailConfig.smtpFrom && emailConfig.smtpUser) { emailConfig.smtpFrom = emailConfig.smtpUser; }
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchConfig: any = {};
|
||||||
|
if (answers.configureSearch) {
|
||||||
|
const searchAnswers = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'password',
|
||||||
|
name: 'tavilyApiKey',
|
||||||
|
message: currentConfig.tavilyApiKey
|
||||||
|
? `Tavily API Key (Leave empty to keep ${maskSecret(currentConfig.tavilyApiKey)}):`
|
||||||
|
: 'Tavily API Key (Free at tavily.com):',
|
||||||
|
mask: '*'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
searchConfig = { tavilyApiKey: searchAnswers.tavilyApiKey || currentConfig.tavilyApiKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
let notifyConfig: any = {};
|
||||||
|
if (answers.configureNotify) {
|
||||||
|
const notifyAnswers = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'password',
|
||||||
|
name: 'feishuWebhook',
|
||||||
|
message: currentConfig.feishuWebhook
|
||||||
|
? `Feishu Webhook (Leave empty to keep ${maskSecret(currentConfig.feishuWebhook)}):`
|
||||||
|
: 'Feishu Webhook (Optional):',
|
||||||
|
mask: '*'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'password',
|
||||||
|
name: 'dingtalkWebhook',
|
||||||
|
message: currentConfig.dingtalkWebhook
|
||||||
|
? `DingTalk Webhook (Leave empty to keep ${maskSecret(currentConfig.dingtalkWebhook)}):`
|
||||||
|
: 'DingTalk Webhook (Optional):',
|
||||||
|
mask: '*'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'password',
|
||||||
|
name: 'wecomWebhook',
|
||||||
|
message: currentConfig.wecomWebhook
|
||||||
|
? `WeCom Webhook (Leave empty to keep ${maskSecret(currentConfig.wecomWebhook)}):`
|
||||||
|
: 'WeCom Webhook (Optional):',
|
||||||
|
mask: '*'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
notifyConfig = {
|
||||||
|
feishuWebhook: notifyAnswers.feishuWebhook || currentConfig.feishuWebhook,
|
||||||
|
dingtalkWebhook: notifyAnswers.dingtalkWebhook || currentConfig.dingtalkWebhook,
|
||||||
|
wecomWebhook: notifyAnswers.wecomWebhook || currentConfig.wecomWebhook
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newConfig: AppConfig = {
|
||||||
|
apiKey: finalApiKey,
|
||||||
|
baseUrl: answers.baseUrl,
|
||||||
|
model: answers.model,
|
||||||
|
...emailConfig,
|
||||||
|
...searchConfig,
|
||||||
|
...notifyConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(GLOBAL_CONFIG_DIR)) {
|
||||||
|
fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(GLOBAL_CONFIG_FILE, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
|
||||||
|
console.log(chalk.green(`\n✅ Configuration saved to ${GLOBAL_CONFIG_FILE}`));
|
||||||
|
console.log(chalk.cyan("You can now run 'autoclaw' to start using the agent."));
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(chalk.red(`Failed to write config: ${error.message}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runChat(queryParts: string[], options: any) {
|
||||||
|
if (options.interactive) {
|
||||||
|
console.log(chalk.bold.cyan("Welcome to AutoClaw CLI 🦞"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialQuery = queryParts.join(' ');
|
||||||
|
|
||||||
|
// 1. Load Global JSON
|
||||||
|
const globalConfig = loadJsonConfig(GLOBAL_CONFIG_FILE);
|
||||||
|
|
||||||
|
// 2. Load Local JSON (Project Level)
|
||||||
|
const localConfig = loadJsonConfig(LOCAL_CONFIG_FILE);
|
||||||
|
if (Object.keys(localConfig).length > 0 && options.interactive) {
|
||||||
|
console.log(chalk.dim(`Loaded project config from ${LOCAL_CONFIG_FILE}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Merge Configs for Tool Usage
|
||||||
|
// Priority: Local > Global
|
||||||
|
const fullConfig = { ...globalConfig, ...localConfig };
|
||||||
|
|
||||||
|
// 4. Resolve Env Vars (CLI > Env > Config)
|
||||||
|
let apiKey = process.env.OPENAI_API_KEY || fullConfig.apiKey;
|
||||||
|
let baseURL = process.env.OPENAI_BASE_URL || fullConfig.baseUrl;
|
||||||
|
let model = options.model || process.env.OPENAI_MODEL || fullConfig.model || 'gpt-4o';
|
||||||
|
|
||||||
|
// Inject Runtime Flags
|
||||||
|
fullConfig.autoConfirm = options.yes;
|
||||||
|
|
||||||
|
// Inject Env vars
|
||||||
|
if (process.env.SMTP_HOST) fullConfig.smtpHost = process.env.SMTP_HOST;
|
||||||
|
if (process.env.SMTP_PORT) fullConfig.smtpPort = process.env.SMTP_PORT;
|
||||||
|
if (process.env.SMTP_User) fullConfig.smtpUser = process.env.SMTP_USER;
|
||||||
|
if (process.env.SMTP_PASS) fullConfig.smtpPass = process.env.SMTP_PASS;
|
||||||
|
if (process.env.TAVILY_API_KEY) fullConfig.tavilyApiKey = process.env.TAVILY_API_KEY;
|
||||||
|
if (process.env.FEISHU_WEBHOOK) fullConfig.feishuWebhook = process.env.FEISHU_WEBHOOK;
|
||||||
|
if (process.env.DINGTALK_WEBHOOK) fullConfig.dingtalkWebhook = process.env.DINGTALK_WEBHOOK;
|
||||||
|
if (process.env.WECOM_WEBHOOK) fullConfig.wecomWebhook = process.env.WECOM_WEBHOOK;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
console.log(chalk.yellow("API Key not found."));
|
||||||
|
const { doSetup } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'doSetup',
|
||||||
|
message: 'Would you like to run the setup wizard now?',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (doSetup) {
|
||||||
|
await runSetup();
|
||||||
|
const newConfig = loadJsonConfig(GLOBAL_CONFIG_FILE);
|
||||||
|
apiKey = newConfig.apiKey;
|
||||||
|
baseURL = newConfig.baseUrl;
|
||||||
|
model = options.model || newConfig.model || 'gpt-4o';
|
||||||
|
Object.assign(fullConfig, newConfig);
|
||||||
|
} else {
|
||||||
|
console.error(chalk.red("API Key is required to proceed."));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
console.error(chalk.red("API Key is still missing. Exiting."));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = new Agent(apiKey!, baseURL, model, fullConfig);
|
||||||
|
|
||||||
|
if (options.interactive) {
|
||||||
|
console.log(chalk.green(`Agent initialized with model: ${model}`));
|
||||||
|
console.log(chalk.gray("Type 'exit' or 'quit' to leave."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle initial query if present
|
||||||
|
if (initialQuery) {
|
||||||
|
if (options.interactive) {
|
||||||
|
console.log(chalk.blue("\nProcessing initial request: ") + chalk.bold(initialQuery));
|
||||||
|
}
|
||||||
|
await agent.chat(initialQuery);
|
||||||
|
|
||||||
|
// Headless mode exit
|
||||||
|
if (!options.interactive) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main chat loop
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { userInput } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'userInput',
|
||||||
|
message: 'You >'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (userInput.toLowerCase() === 'exit' || userInput.toLowerCase() === 'quit') {
|
||||||
|
console.log(chalk.cyan("Goodbye!"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userInput.trim() === '') continue;
|
||||||
|
|
||||||
|
await agent.chat(userInput);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
// Check for Inquirer interruption error (Ctrl+C often causes this)
|
||||||
|
if (err.message && (err.message.includes('User force closed') || err.message.includes('Prompt was canceled'))) {
|
||||||
|
console.log(chalk.cyan("\nGoodbye!"));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
throw err; // Re-throw real errors to be caught by main().catch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global error handler
|
||||||
|
main().catch(err => {
|
||||||
|
if (err.message && (err.message.includes('User force closed') || err.message.includes('Prompt was canceled'))) {
|
||||||
|
console.log(chalk.cyan("\nGoodbye!"));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
console.error(chalk.red("Fatal Error:"), err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Just a wrapper to keep the promise chain clean if needed,
|
||||||
|
// but currently logic is triggered by program.parse()
|
||||||
|
}
|
||||||
109
src/tools/core.ts
Normal file
109
src/tools/core.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
|
import inquirer from 'inquirer';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import util from 'util';
|
||||||
|
import { ToolModule } from './interface.js';
|
||||||
|
|
||||||
|
const execAsync = util.promisify(exec);
|
||||||
|
|
||||||
|
export const ShellTool: ToolModule = {
|
||||||
|
name: "Shell Execution",
|
||||||
|
definition: {
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "execute_shell_command",
|
||||||
|
description: "Execute a shell command on the host machine. Use this to run scripts, list files, or interact with the system.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
command: { type: "string", description: "The shell command to execute." },
|
||||||
|
rationale: { type: "string", description: "Explain why you are running this command." }
|
||||||
|
},
|
||||||
|
required: ["command", "rationale"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (args: any, config: any) => {
|
||||||
|
console.log(chalk.yellow(`\nAI wants to execute: `) + chalk.bold(args.command));
|
||||||
|
console.log(chalk.dim(`Reason: ${args.rationale}`));
|
||||||
|
|
||||||
|
// Check for auto-confirm flag
|
||||||
|
if (!config?.autoConfirm) {
|
||||||
|
const { confirm } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'confirm',
|
||||||
|
message: 'Do you want to run this command?',
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!confirm) return "User denied command execution.";
|
||||||
|
} else {
|
||||||
|
console.log(chalk.gray("(Auto-confirming command execution due to --yes flag)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await execAsync(args.command);
|
||||||
|
return stdout + (stderr ? `\nStderr: ${stderr}` : '');
|
||||||
|
} catch (error: any) {
|
||||||
|
return `Command failed: ${error.message}\nStdout: ${error.stdout}\nStderr: ${error.stderr}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReadFileTool: ToolModule = {
|
||||||
|
name: "File Reader",
|
||||||
|
definition: {
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "read_file",
|
||||||
|
description: "Read the content of a file.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
path: { type: "string", description: "The path to the file to read." }
|
||||||
|
},
|
||||||
|
required: ["path"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(args.path, 'utf-8');
|
||||||
|
return content;
|
||||||
|
} catch (error: any) {
|
||||||
|
return `Error reading file: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WriteFileTool: ToolModule = {
|
||||||
|
name: "File Writer",
|
||||||
|
definition: {
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "write_file",
|
||||||
|
description: "Write content to a file. Overwrites existing files.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
path: { type: "string", description: "The path to the file to write." },
|
||||||
|
content: { type: "string", description: "The content to write." }
|
||||||
|
},
|
||||||
|
required: ["path", "content"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
try {
|
||||||
|
await fs.mkdir(path.dirname(args.path), { recursive: true });
|
||||||
|
await fs.writeFile(args.path, args.content, 'utf-8');
|
||||||
|
return `Successfully wrote to ${args.path}`;
|
||||||
|
} catch (error: any) {
|
||||||
|
return `Error writing file: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
55
src/tools/email.ts
Normal file
55
src/tools/email.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import { ToolModule } from './interface.js';
|
||||||
|
|
||||||
|
export const EmailTool: ToolModule = {
|
||||||
|
name: "Email Service",
|
||||||
|
configKeys: ["smtpHost", "smtpPort", "smtpUser", "smtpPass", "smtpFrom"],
|
||||||
|
definition: {
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "send_email",
|
||||||
|
description: "Send an email using configured SMTP settings.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
to: { type: "string", description: "Recipient email address." },
|
||||||
|
subject: { type: "string", description: "Email subject." },
|
||||||
|
body: { type: "string", description: "Email body content (text)." }
|
||||||
|
},
|
||||||
|
required: ["to", "subject", "body"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (args: any, config: any) => {
|
||||||
|
// Validate config
|
||||||
|
if (!config?.smtpHost || !config?.smtpUser || !config?.smtpPass) {
|
||||||
|
return "Error: Email tool is not configured. Please run 'autoclaw setup' to configure SMTP settings.";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: config.smtpHost,
|
||||||
|
port: parseInt(config.smtpPort || '587'),
|
||||||
|
secure: parseInt(config.smtpPort) === 465, // true for 465, false for other ports
|
||||||
|
auth: {
|
||||||
|
user: config.smtpUser,
|
||||||
|
pass: config.smtpPass,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: config.smtpFrom || config.smtpUser, // sender address
|
||||||
|
to: args.to, // list of receivers
|
||||||
|
subject: args.subject, // Subject line
|
||||||
|
text: args.body, // plain text body
|
||||||
|
});
|
||||||
|
|
||||||
|
return `Email sent successfully. Message ID: ${info.messageId}`;
|
||||||
|
} catch (error: any) {
|
||||||
|
// Return detailed error info for debugging
|
||||||
|
const code = error.code ? `[Code: ${error.code}] ` : '';
|
||||||
|
const response = error.response ? ` (Server Response: ${error.response})` : '';
|
||||||
|
return `Failed to send email: ${code}${error.message}${response}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
28
src/tools/index.ts
Normal file
28
src/tools/index.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { ToolModule } from './interface.js';
|
||||||
|
import { ShellTool, ReadFileTool, WriteFileTool } from './core.js';
|
||||||
|
import { EmailTool } from './email.js';
|
||||||
|
import { SearchTool } from './search.js';
|
||||||
|
import { NotifyTool } from './notify.js';
|
||||||
|
|
||||||
|
// Central Registry of all available tools
|
||||||
|
export const toolRegistry: ToolModule[] = [
|
||||||
|
ShellTool,
|
||||||
|
ReadFileTool,
|
||||||
|
WriteFileTool,
|
||||||
|
EmailTool,
|
||||||
|
SearchTool,
|
||||||
|
NotifyTool
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getToolDefinitions() {
|
||||||
|
return toolRegistry.map(t => t.definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function executeToolHandler(name: string, args: any, fullConfig: any): Promise<string> {
|
||||||
|
const tool = toolRegistry.find(t => t.definition.function.name === name);
|
||||||
|
if (!tool) {
|
||||||
|
return `Error: Tool ${name} not found.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await tool.handler(args, fullConfig);
|
||||||
|
}
|
||||||
19
src/tools/interface.ts
Normal file
19
src/tools/interface.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
export interface ToolDefinition {
|
||||||
|
type: "function";
|
||||||
|
function: {
|
||||||
|
name: "execute_shell_command" | "read_file" | "write_file" | "send_email" | string;
|
||||||
|
description: string;
|
||||||
|
parameters: {
|
||||||
|
type: "object";
|
||||||
|
properties: Record<string, any>;
|
||||||
|
required: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolModule {
|
||||||
|
name: string; // Display name for setup (e.g., "Email Service")
|
||||||
|
configKeys?: string[]; // Keys needed in setting.json (e.g., ["smtpHost", "smtpUser"])
|
||||||
|
definition: ToolDefinition; // OpenAI Tool Definition
|
||||||
|
handler: (args: any, config?: any) => Promise<string>; // Implementation
|
||||||
|
}
|
||||||
90
src/tools/notify.ts
Normal file
90
src/tools/notify.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { ToolModule } from './interface.js';
|
||||||
|
|
||||||
|
export const NotifyTool: ToolModule = {
|
||||||
|
name: "Group Bot Notification",
|
||||||
|
configKeys: ["feishuWebhook", "dingtalkWebhook", "wecomWebhook"],
|
||||||
|
definition: {
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "send_notification",
|
||||||
|
description: "Send a text message to an Instant Messaging (IM) Group Bot (Feishu/Lark, DingTalk, WeCom).",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
platform: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["feishu", "dingtalk", "wecom"],
|
||||||
|
description: "The target platform."
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: "string",
|
||||||
|
description: "The text content to send."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["platform", "content"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (args: any, config: any) => {
|
||||||
|
const { platform, content } = args;
|
||||||
|
let webhookUrl = '';
|
||||||
|
let payload = {};
|
||||||
|
|
||||||
|
// 1. Determine Webhook URL and Payload Format
|
||||||
|
switch (platform) {
|
||||||
|
case 'feishu':
|
||||||
|
webhookUrl = config.feishuWebhook || process.env.FEISHU_WEBHOOK;
|
||||||
|
if (!webhookUrl) return "Error: Feishu Webhook URL is not configured.";
|
||||||
|
payload = {
|
||||||
|
msg_type: "text",
|
||||||
|
content: { text: content }
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'dingtalk':
|
||||||
|
webhookUrl = config.dingtalkWebhook || process.env.DINGTALK_WEBHOOK;
|
||||||
|
if (!webhookUrl) return "Error: DingTalk Webhook URL is not configured.";
|
||||||
|
payload = {
|
||||||
|
msgtype: "text",
|
||||||
|
text: { content: content }
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'wecom':
|
||||||
|
webhookUrl = config.wecomWebhook || process.env.WECOM_WEBHOOK;
|
||||||
|
if (!webhookUrl) return "Error: WeCom Webhook URL is not configured.";
|
||||||
|
payload = {
|
||||||
|
msgtype: "text",
|
||||||
|
text: { content: content }
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return `Error: Unknown platform '${platform}'. Supported: feishu, dingtalk, wecom.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Send Request
|
||||||
|
try {
|
||||||
|
const response = await fetch(webhookUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: any = await response.json();
|
||||||
|
|
||||||
|
// Platform specific success checks
|
||||||
|
// Feishu: code 0
|
||||||
|
// DingTalk: errcode 0
|
||||||
|
// WeCom: errcode 0
|
||||||
|
if (result.code === 0 || result.errcode === 0) {
|
||||||
|
return `Notification sent to ${platform} successfully.`;
|
||||||
|
} else {
|
||||||
|
return `Failed to send to ${platform}. API Response: ${JSON.stringify(result)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
return `Network error sending notification: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
87
src/tools/search.ts
Normal file
87
src/tools/search.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { ToolModule } from './interface.js';
|
||||||
|
|
||||||
|
export const SearchTool: ToolModule = {
|
||||||
|
name: "Web Search (Tavily)",
|
||||||
|
configKeys: ["tavilyApiKey"],
|
||||||
|
definition: {
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "web_search",
|
||||||
|
description: "Search the web for real-time information. Returns a summary of search results.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: "string",
|
||||||
|
description: "The search query (e.g., 'latest openclaw news', 'nodejs documentation')."
|
||||||
|
},
|
||||||
|
depth: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["basic", "advanced"],
|
||||||
|
description: "Search depth. 'basic' is faster, 'advanced' scrapes more content."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["query"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (args: any, config: any) => {
|
||||||
|
const apiKey = config.tavilyApiKey || process.env.TAVILY_API_KEY;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return "Error: Tavily API Key is missing. Please run 'autoclaw setup' to configure it, or set TAVILY_API_KEY env var. Get a free key at https://tavily.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://api.tavily.com/search", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
api_key: apiKey,
|
||||||
|
query: args.query,
|
||||||
|
search_depth: args.depth || "basic",
|
||||||
|
include_answer: true,
|
||||||
|
include_images: false,
|
||||||
|
max_results: 5
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errText = await response.text();
|
||||||
|
return `Search API Error: ${response.status} - ${errText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: any = await response.json();
|
||||||
|
|
||||||
|
// Format the results beautifully for the LLM
|
||||||
|
let output = `Search Results for "${args.query}":
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (data.answer) {
|
||||||
|
output += `💡 **Direct Answer**: ${data.answer}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.results && Array.isArray(data.results)) {
|
||||||
|
data.results.forEach((result: any, index: number) => {
|
||||||
|
output += `### ${index + 1}. ${result.title}
|
||||||
|
`;
|
||||||
|
output += `🔗 ${result.url}
|
||||||
|
`;
|
||||||
|
output += `📝 ${result.content}
|
||||||
|
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
return `Failed to perform web search: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue