This commit is contained in:
tsingliu 2026-02-06 18:41:02 +08:00
commit 0d43b43998
19 changed files with 2238 additions and 0 deletions

3
.env.example Normal file
View 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
View file

@ -0,0 +1,3 @@
.env
node_modules
dist

8
.npmignore Normal file
View file

@ -0,0 +1,8 @@
src/
tsconfig.json
.env
.env.example
install.bat
install.sh
GEMINI.md
node_modules/

81
GEMINI.md Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

49
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"]
}