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