autoclaw 1.0.5 → 1.0.7

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/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()})
@@ -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
@@ -8,6 +8,11 @@ import * as fs from 'fs';
8
8
  import * as path from 'path';
9
9
  import * as os from 'os';
10
10
  import { fileURLToPath } from 'url';
11
+ // Handle Ctrl+C gracefully
12
+ process.on('SIGINT', () => {
13
+ console.log(chalk.cyan("\n\nGoodbye! (Interrupted)"));
14
+ process.exit(0);
15
+ });
11
16
  const GLOBAL_CONFIG_DIR = path.join(os.homedir(), '.autoclaw');
12
17
  const GLOBAL_CONFIG_FILE = path.join(GLOBAL_CONFIG_DIR, 'setting.json');
13
18
  const LOCAL_CONFIG_FILE = path.join(process.cwd(), '.autoclaw', 'setting.json');
@@ -22,7 +27,7 @@ function loadJsonConfig(filePath) {
22
27
  }
23
28
  return {};
24
29
  }
25
- // Load local env vars
30
+ // Load local env vars (lowest priority of env vars, but env vars override JSON)
26
31
  dotenv.config();
27
32
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
28
33
  // In dist/index.js, package.json is usually up one level in the root
@@ -78,12 +83,55 @@ async function runSetup() {
78
83
  name: 'model',
79
84
  message: 'Enter default Model:',
80
85
  default: currentConfig.model || 'gpt-4o'
86
+ },
87
+ {
88
+ type: 'confirm',
89
+ name: 'configureEmail',
90
+ message: 'Do you want to configure the Email Tool (SMTP)?',
91
+ default: !!currentConfig.smtpHost
81
92
  }
82
93
  ]);
94
+ let emailConfig = {};
95
+ if (answers.configureEmail) {
96
+ emailConfig = await inquirer.prompt([
97
+ {
98
+ type: 'input',
99
+ name: 'smtpHost',
100
+ message: 'SMTP Host:',
101
+ default: currentConfig.smtpHost
102
+ },
103
+ {
104
+ type: 'input',
105
+ name: 'smtpPort',
106
+ message: 'SMTP Port:',
107
+ default: currentConfig.smtpPort || '587'
108
+ },
109
+ {
110
+ type: 'input',
111
+ name: 'smtpUser',
112
+ message: 'SMTP Username:',
113
+ default: currentConfig.smtpUser
114
+ },
115
+ {
116
+ type: 'password',
117
+ name: 'smtpPass',
118
+ message: 'SMTP Password:',
119
+ mask: '*',
120
+ default: currentConfig.smtpPass
121
+ },
122
+ {
123
+ type: 'input',
124
+ name: 'smtpFrom',
125
+ message: 'Sender Email Address (From):',
126
+ default: currentConfig.smtpFrom || emailConfig.smtpUser
127
+ }
128
+ ]);
129
+ }
83
130
  const newConfig = {
84
131
  apiKey: answers.apiKey,
85
132
  baseUrl: answers.baseUrl,
86
- model: answers.model
133
+ model: answers.model,
134
+ ...emailConfig
87
135
  };
88
136
  try {
89
137
  if (!fs.existsSync(GLOBAL_CONFIG_DIR)) {
@@ -106,10 +154,22 @@ async function runChat(options) {
106
154
  if (Object.keys(localConfig).length > 0) {
107
155
  console.log(chalk.dim(`Loaded project config from ${LOCAL_CONFIG_FILE}`));
108
156
  }
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';
157
+ // 3. Merge Configs for Tool Usage
158
+ // Priority: Local > Global
159
+ const fullConfig = { ...globalConfig, ...localConfig };
160
+ // 4. Resolve Env Vars (CLI > Env > Config)
161
+ let apiKey = process.env.OPENAI_API_KEY || fullConfig.apiKey;
162
+ let baseURL = process.env.OPENAI_BASE_URL || fullConfig.baseUrl;
163
+ let model = options.model || process.env.OPENAI_MODEL || fullConfig.model || 'gpt-4o';
164
+ // Inject Env vars for SMTP if present (optional but good for CI/CD)
165
+ if (process.env.SMTP_HOST)
166
+ fullConfig.smtpHost = process.env.SMTP_HOST;
167
+ if (process.env.SMTP_PORT)
168
+ fullConfig.smtpPort = process.env.SMTP_PORT;
169
+ if (process.env.SMTP_USER)
170
+ fullConfig.smtpUser = process.env.SMTP_USER;
171
+ if (process.env.SMTP_PASS)
172
+ fullConfig.smtpPass = process.env.SMTP_PASS;
113
173
  if (!apiKey) {
114
174
  console.log(chalk.yellow("API Key not found."));
115
175
  const { doSetup } = await inquirer.prompt([
@@ -126,6 +186,7 @@ async function runChat(options) {
126
186
  apiKey = newConfig.apiKey;
127
187
  baseURL = newConfig.baseUrl;
128
188
  model = options.model || newConfig.model || 'gpt-4o';
189
+ Object.assign(fullConfig, newConfig);
129
190
  }
130
191
  else {
131
192
  console.error(chalk.red("API Key is required to proceed."));
@@ -136,23 +197,47 @@ async function runChat(options) {
136
197
  console.error(chalk.red("API Key is still missing. Exiting."));
137
198
  process.exit(1);
138
199
  }
139
- const agent = new Agent(apiKey, baseURL, model);
200
+ const agent = new Agent(apiKey, baseURL, model, fullConfig);
140
201
  console.log(chalk.green(`Agent initialized with model: ${model}`));
141
202
  console.log(chalk.gray("Type 'exit' or 'quit' to leave."));
142
- while (true) {
143
- const { userInput } = await inquirer.prompt([
144
- {
145
- type: 'input',
146
- name: 'userInput',
147
- message: 'You >'
203
+ // Main chat loop
204
+ try {
205
+ while (true) {
206
+ const { userInput } = await inquirer.prompt([
207
+ {
208
+ type: 'input',
209
+ name: 'userInput',
210
+ message: 'You >'
211
+ }
212
+ ]);
213
+ if (userInput.toLowerCase() === 'exit' || userInput.toLowerCase() === 'quit') {
214
+ console.log(chalk.cyan("Goodbye!"));
215
+ break;
148
216
  }
149
- ]);
150
- if (userInput.toLowerCase() === 'exit' || userInput.toLowerCase() === 'quit') {
151
- console.log(chalk.cyan("Goodbye!"));
152
- break;
217
+ if (userInput.trim() === '')
218
+ continue;
219
+ await agent.chat(userInput);
220
+ }
221
+ }
222
+ catch (err) {
223
+ // Check for Inquirer interruption error (Ctrl+C often causes this)
224
+ if (err.message && (err.message.includes('User force closed') || err.message.includes('Prompt was canceled'))) {
225
+ console.log(chalk.cyan("\nGoodbye!"));
226
+ process.exit(0);
153
227
  }
154
- if (userInput.trim() === '')
155
- continue;
156
- await agent.chat(userInput);
228
+ throw err; // Re-throw real errors to be caught by main().catch
157
229
  }
158
230
  }
231
+ // Global error handler
232
+ main().catch(err => {
233
+ if (err.message && (err.message.includes('User force closed') || err.message.includes('Prompt was canceled'))) {
234
+ console.log(chalk.cyan("\nGoodbye!"));
235
+ process.exit(0);
236
+ }
237
+ console.error(chalk.red("Fatal Error:"), err);
238
+ process.exit(1);
239
+ });
240
+ async function main() {
241
+ // Just a wrapper to keep the promise chain clean if needed,
242
+ // but currently logic is triggered by program.parse()
243
+ }
@@ -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.5",
3
+ "version": "1.0.7",
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
  }