autoclaw 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -6
- package/dist/agent.js +18 -16
- package/dist/index.js +63 -7
- package/dist/tools/core.js +104 -0
- package/dist/tools/email.js +48 -0
- package/dist/tools/index.js +29 -0
- package/dist/tools/interface.js +1 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
# AutoClaw 🦞
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**The Docker-Native Headless Agent for Massive Scale Automation.**
|
|
4
|
+
|
|
5
|
+
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**.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
## Why AutoClaw?
|
|
10
|
+
- 🐳 **Docker Native**: Built to run safely inside containers. Minimal footprint (Node.js/Alpine friendly).
|
|
11
|
+
- 🚀 **Massive Scalability**: Text-only, headless design means you can spawn thousands of agents without consuming graphical resources.
|
|
12
|
+
- 🛡️ **Sandbox Safety**: Ideal for running untrusted code when isolated in Docker.
|
|
13
|
+
- 🔌 **Swarm Ready**: Stateless design allows for easy orchestration via K8s, Docker Swarm, or simple shell loops.
|
|
4
14
|
|
|
5
15
|
## Features
|
|
6
16
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- 📂 **
|
|
10
|
-
- 🧠 **Context Aware**:
|
|
11
|
-
- 🔌 **Model Agnostic**: Compatible with OpenAI, DeepSeek, LocalLLM, or any OpenAI-compatible API.
|
|
17
|
+
- 📜 **Headless Execution**: No browsers, no GUIs. Pure terminal efficiency.
|
|
18
|
+
- 🤖 **Non-Interactive**: Intelligent flag handling (`-y`) for zero-touch automation.
|
|
19
|
+
- 📂 **Universal Control**: From simple file I/O to complex system administration.
|
|
20
|
+
- 🧠 **Context Aware**: Detects container environments to optimize command strategies.
|
|
12
21
|
|
|
13
22
|
## Installation
|
|
14
23
|
|
package/dist/agent.js
CHANGED
|
@@ -2,17 +2,19 @@ import OpenAI from 'openai';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import * as os from 'os';
|
|
5
|
-
import {
|
|
5
|
+
import { getToolDefinitions, executeToolHandler } from './tools/index.js';
|
|
6
6
|
export class Agent {
|
|
7
7
|
client;
|
|
8
8
|
messages;
|
|
9
9
|
model;
|
|
10
|
-
|
|
10
|
+
config;
|
|
11
|
+
constructor(apiKey, baseURL, model = 'gpt-4-turbo-preview', config = {}) {
|
|
11
12
|
this.client = new OpenAI({
|
|
12
13
|
apiKey: apiKey,
|
|
13
14
|
baseURL: baseURL
|
|
14
15
|
});
|
|
15
16
|
this.model = model;
|
|
17
|
+
this.config = config;
|
|
16
18
|
const systemInfo = `
|
|
17
19
|
System Information:
|
|
18
20
|
- OS: ${os.type()} ${os.release()} (${os.platform()})
|
|
@@ -25,22 +27,22 @@ System Information:
|
|
|
25
27
|
this.messages = [
|
|
26
28
|
{
|
|
27
29
|
role: "system",
|
|
28
|
-
content: `You are AutoClaw,
|
|
29
|
-
|
|
30
|
-
You have access to tools to execute shell commands and read/write files.
|
|
30
|
+
content: `You are AutoClaw, a Docker-Native Autonomous Agent designed for massive scale automation.
|
|
31
|
+
You are likely running inside a container or headless server, possibly as one of thousands of parallel units in a swarm.
|
|
31
32
|
|
|
32
33
|
CONTEXT:
|
|
33
34
|
${systemInfo}
|
|
34
35
|
|
|
36
|
+
ENVIRONMENT CONSTRAINTS:
|
|
37
|
+
1. HEADLESS: No GUI available. Do not try to open browsers or apps.
|
|
38
|
+
2. CONTAINER-OPTIMIZED: Assume you are in a sandbox. You can be aggressive with file creation but robust with errors.
|
|
39
|
+
3. NON-INTERACTIVE: Always use flags to suppress prompts (e.g., 'apt-get -y', 'rm -rf').
|
|
40
|
+
|
|
35
41
|
GUIDELINES:
|
|
36
|
-
1.
|
|
37
|
-
2.
|
|
38
|
-
3.
|
|
39
|
-
4.
|
|
40
|
-
5. If you need to write a file, use 'write_file'.
|
|
41
|
-
6. Be concise in your responses.
|
|
42
|
-
7. If the user's request is ambiguous, ask for clarification.
|
|
43
|
-
8. You are running on the user's local machine. Be careful with destructive commands (rm, etc.).
|
|
42
|
+
1. EFFICIENCY: Your goal is speed and success. Write scripts that just work.
|
|
43
|
+
2. ROBUSTNESS: Use standard Linux/Unix tools found in minimal images (Alpine/Debian).
|
|
44
|
+
3. TOOLS: Use 'execute_shell_command' for actions, 'write_file' for code generation.
|
|
45
|
+
4. CLARITY: Output concise logs. You are a worker unit, not a chat bot.
|
|
44
46
|
`
|
|
45
47
|
}
|
|
46
48
|
];
|
|
@@ -54,7 +56,7 @@ GUIDELINES:
|
|
|
54
56
|
const response = await this.client.chat.completions.create({
|
|
55
57
|
model: this.model,
|
|
56
58
|
messages: this.messages,
|
|
57
|
-
tools:
|
|
59
|
+
tools: getToolDefinitions(),
|
|
58
60
|
tool_choice: "auto"
|
|
59
61
|
});
|
|
60
62
|
spinner.stop();
|
|
@@ -70,13 +72,13 @@ GUIDELINES:
|
|
|
70
72
|
const functionName = toolCall.function.name;
|
|
71
73
|
const functionArgs = JSON.parse(toolCall.function.arguments);
|
|
72
74
|
console.log(chalk.gray(`Executing tool: ${functionName}...`));
|
|
73
|
-
|
|
75
|
+
// Pass the full config to the tool handler
|
|
76
|
+
const toolResult = await executeToolHandler(functionName, functionArgs, this.config);
|
|
74
77
|
this.messages.push({
|
|
75
78
|
role: "tool",
|
|
76
79
|
tool_call_id: toolCall.id,
|
|
77
80
|
content: toolResult
|
|
78
81
|
});
|
|
79
|
-
// console.log(chalk.dim(`Tool Output: ${toolResult.slice(0, 100)}...`));
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
else {
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@ function loadJsonConfig(filePath) {
|
|
|
22
22
|
}
|
|
23
23
|
return {};
|
|
24
24
|
}
|
|
25
|
-
// Load local env vars
|
|
25
|
+
// Load local env vars (lowest priority of env vars, but env vars override JSON)
|
|
26
26
|
dotenv.config();
|
|
27
27
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
28
|
// In dist/index.js, package.json is usually up one level in the root
|
|
@@ -78,12 +78,55 @@ async function runSetup() {
|
|
|
78
78
|
name: 'model',
|
|
79
79
|
message: 'Enter default Model:',
|
|
80
80
|
default: currentConfig.model || 'gpt-4o'
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
type: 'confirm',
|
|
84
|
+
name: 'configureEmail',
|
|
85
|
+
message: 'Do you want to configure the Email Tool (SMTP)?',
|
|
86
|
+
default: !!currentConfig.smtpHost
|
|
81
87
|
}
|
|
82
88
|
]);
|
|
89
|
+
let emailConfig = {};
|
|
90
|
+
if (answers.configureEmail) {
|
|
91
|
+
emailConfig = await inquirer.prompt([
|
|
92
|
+
{
|
|
93
|
+
type: 'input',
|
|
94
|
+
name: 'smtpHost',
|
|
95
|
+
message: 'SMTP Host:',
|
|
96
|
+
default: currentConfig.smtpHost
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'input',
|
|
100
|
+
name: 'smtpPort',
|
|
101
|
+
message: 'SMTP Port:',
|
|
102
|
+
default: currentConfig.smtpPort || '587'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: 'input',
|
|
106
|
+
name: 'smtpUser',
|
|
107
|
+
message: 'SMTP Username:',
|
|
108
|
+
default: currentConfig.smtpUser
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
type: 'password',
|
|
112
|
+
name: 'smtpPass',
|
|
113
|
+
message: 'SMTP Password:',
|
|
114
|
+
mask: '*',
|
|
115
|
+
default: currentConfig.smtpPass
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: 'input',
|
|
119
|
+
name: 'smtpFrom',
|
|
120
|
+
message: 'Sender Email Address (From):',
|
|
121
|
+
default: currentConfig.smtpFrom || emailConfig.smtpUser
|
|
122
|
+
}
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
83
125
|
const newConfig = {
|
|
84
126
|
apiKey: answers.apiKey,
|
|
85
127
|
baseUrl: answers.baseUrl,
|
|
86
|
-
model: answers.model
|
|
128
|
+
model: answers.model,
|
|
129
|
+
...emailConfig
|
|
87
130
|
};
|
|
88
131
|
try {
|
|
89
132
|
if (!fs.existsSync(GLOBAL_CONFIG_DIR)) {
|
|
@@ -106,10 +149,22 @@ async function runChat(options) {
|
|
|
106
149
|
if (Object.keys(localConfig).length > 0) {
|
|
107
150
|
console.log(chalk.dim(`Loaded project config from ${LOCAL_CONFIG_FILE}`));
|
|
108
151
|
}
|
|
109
|
-
// 3. Merge
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
152
|
+
// 3. Merge Configs for Tool Usage
|
|
153
|
+
// Priority: Local > Global
|
|
154
|
+
const fullConfig = { ...globalConfig, ...localConfig };
|
|
155
|
+
// 4. Resolve Env Vars (CLI > Env > Config)
|
|
156
|
+
let apiKey = process.env.OPENAI_API_KEY || fullConfig.apiKey;
|
|
157
|
+
let baseURL = process.env.OPENAI_BASE_URL || fullConfig.baseUrl;
|
|
158
|
+
let model = options.model || process.env.OPENAI_MODEL || fullConfig.model || 'gpt-4o';
|
|
159
|
+
// Inject Env vars for SMTP if present (optional but good for CI/CD)
|
|
160
|
+
if (process.env.SMTP_HOST)
|
|
161
|
+
fullConfig.smtpHost = process.env.SMTP_HOST;
|
|
162
|
+
if (process.env.SMTP_PORT)
|
|
163
|
+
fullConfig.smtpPort = process.env.SMTP_PORT;
|
|
164
|
+
if (process.env.SMTP_USER)
|
|
165
|
+
fullConfig.smtpUser = process.env.SMTP_USER;
|
|
166
|
+
if (process.env.SMTP_PASS)
|
|
167
|
+
fullConfig.smtpPass = process.env.SMTP_PASS;
|
|
113
168
|
if (!apiKey) {
|
|
114
169
|
console.log(chalk.yellow("API Key not found."));
|
|
115
170
|
const { doSetup } = await inquirer.prompt([
|
|
@@ -126,6 +181,7 @@ async function runChat(options) {
|
|
|
126
181
|
apiKey = newConfig.apiKey;
|
|
127
182
|
baseURL = newConfig.baseUrl;
|
|
128
183
|
model = options.model || newConfig.model || 'gpt-4o';
|
|
184
|
+
Object.assign(fullConfig, newConfig);
|
|
129
185
|
}
|
|
130
186
|
else {
|
|
131
187
|
console.error(chalk.red("API Key is required to proceed."));
|
|
@@ -136,7 +192,7 @@ async function runChat(options) {
|
|
|
136
192
|
console.error(chalk.red("API Key is still missing. Exiting."));
|
|
137
193
|
process.exit(1);
|
|
138
194
|
}
|
|
139
|
-
const agent = new Agent(apiKey, baseURL, model);
|
|
195
|
+
const agent = new Agent(apiKey, baseURL, model, fullConfig);
|
|
140
196
|
console.log(chalk.green(`Agent initialized with model: ${model}`));
|
|
141
197
|
console.log(chalk.gray("Type 'exit' or 'quit' to leave."));
|
|
142
198
|
while (true) {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import util from 'util';
|
|
7
|
+
const execAsync = util.promisify(exec);
|
|
8
|
+
export const ShellTool = {
|
|
9
|
+
name: "Shell Execution",
|
|
10
|
+
definition: {
|
|
11
|
+
type: "function",
|
|
12
|
+
function: {
|
|
13
|
+
name: "execute_shell_command",
|
|
14
|
+
description: "Execute a shell command on the host machine. Use this to run scripts, list files, or interact with the system.",
|
|
15
|
+
parameters: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
command: { type: "string", description: "The shell command to execute." },
|
|
19
|
+
rationale: { type: "string", description: "Explain why you are running this command." }
|
|
20
|
+
},
|
|
21
|
+
required: ["command", "rationale"]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
handler: async (args) => {
|
|
26
|
+
console.log(chalk.yellow(`
|
|
27
|
+
AI wants to execute: `) + chalk.bold(args.command));
|
|
28
|
+
console.log(chalk.dim(`Reason: ${args.rationale}`));
|
|
29
|
+
const { confirm } = await inquirer.prompt([
|
|
30
|
+
{
|
|
31
|
+
type: 'confirm',
|
|
32
|
+
name: 'confirm',
|
|
33
|
+
message: 'Do you want to run this command?',
|
|
34
|
+
default: false
|
|
35
|
+
}
|
|
36
|
+
]);
|
|
37
|
+
if (!confirm)
|
|
38
|
+
return "User denied command execution.";
|
|
39
|
+
try {
|
|
40
|
+
const { stdout, stderr } = await execAsync(args.command);
|
|
41
|
+
return stdout + (stderr ? `
|
|
42
|
+
Stderr: ${stderr}` : '');
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return `Command failed: ${error.message}
|
|
46
|
+
Stdout: ${error.stdout}
|
|
47
|
+
Stderr: ${error.stderr}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
export const ReadFileTool = {
|
|
52
|
+
name: "File Reader",
|
|
53
|
+
definition: {
|
|
54
|
+
type: "function",
|
|
55
|
+
function: {
|
|
56
|
+
name: "read_file",
|
|
57
|
+
description: "Read the content of a file.",
|
|
58
|
+
parameters: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
path: { type: "string", description: "The path to the file to read." }
|
|
62
|
+
},
|
|
63
|
+
required: ["path"]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
handler: async (args) => {
|
|
68
|
+
try {
|
|
69
|
+
const content = await fs.readFile(args.path, 'utf-8');
|
|
70
|
+
return content;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return `Error reading file: ${error.message}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
export const WriteFileTool = {
|
|
78
|
+
name: "File Writer",
|
|
79
|
+
definition: {
|
|
80
|
+
type: "function",
|
|
81
|
+
function: {
|
|
82
|
+
name: "write_file",
|
|
83
|
+
description: "Write content to a file. Overwrites existing files.",
|
|
84
|
+
parameters: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
path: { type: "string", description: "The path to the file to write." },
|
|
88
|
+
content: { type: "string", description: "The content to write." }
|
|
89
|
+
},
|
|
90
|
+
required: ["path", "content"]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
handler: async (args) => {
|
|
95
|
+
try {
|
|
96
|
+
await fs.mkdir(path.dirname(args.path), { recursive: true });
|
|
97
|
+
await fs.writeFile(args.path, args.content, 'utf-8');
|
|
98
|
+
return `Successfully wrote to ${args.path}`;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return `Error writing file: ${error.message}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer';
|
|
2
|
+
export const EmailTool = {
|
|
3
|
+
name: "Email Service",
|
|
4
|
+
configKeys: ["smtpHost", "smtpPort", "smtpUser", "smtpPass", "smtpFrom"],
|
|
5
|
+
definition: {
|
|
6
|
+
type: "function",
|
|
7
|
+
function: {
|
|
8
|
+
name: "send_email",
|
|
9
|
+
description: "Send an email using configured SMTP settings.",
|
|
10
|
+
parameters: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
to: { type: "string", description: "Recipient email address." },
|
|
14
|
+
subject: { type: "string", description: "Email subject." },
|
|
15
|
+
body: { type: "string", description: "Email body content (text)." }
|
|
16
|
+
},
|
|
17
|
+
required: ["to", "subject", "body"]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
handler: async (args, config) => {
|
|
22
|
+
// Validate config
|
|
23
|
+
if (!config?.smtpHost || !config?.smtpUser || !config?.smtpPass) {
|
|
24
|
+
return "Error: Email tool is not configured. Please run 'autoclaw setup' to configure SMTP settings.";
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const transporter = nodemailer.createTransport({
|
|
28
|
+
host: config.smtpHost,
|
|
29
|
+
port: parseInt(config.smtpPort || '587'),
|
|
30
|
+
secure: parseInt(config.smtpPort) === 465, // true for 465, false for other ports
|
|
31
|
+
auth: {
|
|
32
|
+
user: config.smtpUser,
|
|
33
|
+
pass: config.smtpPass,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
const info = await transporter.sendMail({
|
|
37
|
+
from: config.smtpFrom || config.smtpUser, // sender address
|
|
38
|
+
to: args.to, // list of receivers
|
|
39
|
+
subject: args.subject, // Subject line
|
|
40
|
+
text: args.body, // plain text body
|
|
41
|
+
});
|
|
42
|
+
return `Email sent successfully. Message ID: ${info.messageId}`;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return `Failed to send email: ${error.message}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ShellTool, ReadFileTool, WriteFileTool } from './core.js';
|
|
2
|
+
import { EmailTool } from './email.js';
|
|
3
|
+
// Central Registry of all available tools
|
|
4
|
+
export const toolRegistry = [
|
|
5
|
+
ShellTool,
|
|
6
|
+
ReadFileTool,
|
|
7
|
+
WriteFileTool,
|
|
8
|
+
EmailTool
|
|
9
|
+
];
|
|
10
|
+
export function getToolDefinitions() {
|
|
11
|
+
return toolRegistry.map(t => t.definition);
|
|
12
|
+
}
|
|
13
|
+
export async function executeToolHandler(name, args, fullConfig) {
|
|
14
|
+
const tool = toolRegistry.find(t => t.definition.function.name === name);
|
|
15
|
+
if (!tool) {
|
|
16
|
+
return `Error: Tool ${name} not found.`;
|
|
17
|
+
}
|
|
18
|
+
// Pass specific config if needed (e.g., email config)
|
|
19
|
+
// We pass the full config, and let the handler pick what it needs or strictly pass the 'tools' section if structured.
|
|
20
|
+
// For simplicity, passing full config, or we can structure config.tools.email
|
|
21
|
+
// Let's assume config has a 'tools' dictionary.
|
|
22
|
+
const toolConfig = fullConfig.tools ? fullConfig.tools[name] : fullConfig;
|
|
23
|
+
// Actually, let's just pass the root config for now to keep migration simple,
|
|
24
|
+
// or better, pass the 'email' specific config if we structure it.
|
|
25
|
+
// Strategy: In setup, we will save keys like 'smtpHost' at root or under 'tools.email'.
|
|
26
|
+
// Let's keep it flat or structured? Structured is better for plugins.
|
|
27
|
+
// Let's pass the whole config object to the handler, it can pick what it needs.
|
|
28
|
+
return await tool.handler(args, fullConfig);
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autoclaw",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -35,12 +35,14 @@
|
|
|
35
35
|
"commander": "^14.0.3",
|
|
36
36
|
"dotenv": "^16.4.7",
|
|
37
37
|
"inquirer": "^13.2.2",
|
|
38
|
+
"nodemailer": "^8.0.0",
|
|
38
39
|
"openai": "^6.18.0",
|
|
39
40
|
"ora": "^9.3.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@types/inquirer": "^9.0.9",
|
|
43
44
|
"@types/node": "^25.2.1",
|
|
45
|
+
"@types/nodemailer": "^7.0.9",
|
|
44
46
|
"ts-node": "^10.9.2",
|
|
45
47
|
"typescript": "^5.9.3"
|
|
46
48
|
}
|