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 CHANGED
@@ -1,14 +1,23 @@
1
1
  # AutoClaw 🦞
2
2
 
3
- AutoClaw is a lightweight AI agent CLI tool that brings the power of Large Language Models (LLMs) directly to your terminal. It allows you to interact with your file system and execute commands using natural language.
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
- - 💬 **Natural Language Interface**: Chat with your terminal to perform complex tasks.
8
- - 🛡️ **Safe Execution**: All shell commands require user confirmation before execution.
9
- - 📂 **File Management**: Read and write files effortlessly.
10
- - 🧠 **Context Aware**: Automatically detects your OS, shell, and environment for accurate command generation.
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 { tools, executeTool } from './tools.js';
5
+ import { getToolDefinitions, executeToolHandler } from './tools/index.js';
6
6
  export class Agent {
7
7
  client;
8
8
  messages;
9
9
  model;
10
- constructor(apiKey, baseURL, model = 'gpt-4-turbo-preview') {
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, an advanced AI agent running in a CLI environment.
29
- Your goal is to help the user perform tasks on their computer.
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. Use the System Information above to generate platform-specific commands (e.g., use 'dir' for Windows, 'ls' for Linux).
37
- 2. When asked to perform a task, analyze if you need to use tools.
38
- 3. If you need to run a command, always use 'execute_shell_command' and provide a clear rationale.
39
- 4. If you need to read a file, use 'read_file'.
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: 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
- const toolResult = await executeTool(functionName, functionArgs);
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: CLI > Env > Local JSON > Global JSON
110
- let apiKey = process.env.OPENAI_API_KEY || localConfig.apiKey || globalConfig.apiKey;
111
- let baseURL = process.env.OPENAI_BASE_URL || localConfig.baseUrl || globalConfig.baseUrl;
112
- let model = options.model || process.env.OPENAI_MODEL || localConfig.model || globalConfig.model || 'gpt-4o';
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.4",
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
  }