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 +7 -5
- package/dist/index.js +105 -20
- 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/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()})
|
|
@@ -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
|
@@ -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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
}
|