autoclaw 1.3.1 → 1.3.2
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/index.js +102 -32
- package/dist/setup.js +61 -0
- package/dist/tools/core.js +21 -1
- package/dist/tools/email.js +3 -0
- package/dist/tools/image.js +1 -1
- package/dist/tools/notify.js +2 -1
- package/dist/tools/search.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ npm install -g autoclaw
|
|
|
78
78
|
|
|
79
79
|
## Quick Start
|
|
80
80
|
|
|
81
|
-
1. **Setup**: Run the interactive setup wizard to configure your API keys and integrations.
|
|
81
|
+
1. **Setup**: Run the interactive setup wizard to configure your API keys and integrations. The wizard runs a live connection test (failures map to the likely wrong field: 401 = key, 404 = base URL, 400 = model name) and can list the provider's models for you to pick from.
|
|
82
82
|
```bash
|
|
83
83
|
autoclaw setup
|
|
84
84
|
```
|
package/README.zh-CN.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import dotenv from 'dotenv';
|
|
|
6
6
|
import { Agent } from './agent.js';
|
|
7
7
|
import { parseManifest, runBatch } from './batch.js';
|
|
8
8
|
import { PROVIDER_PRESETS, providerNames, resolveProvider } from './providers.js';
|
|
9
|
+
import { fetchModelIds, normalizeBaseUrl, testConnection } from './setup.js';
|
|
9
10
|
import * as fs from 'fs';
|
|
10
11
|
import * as path from 'path';
|
|
11
12
|
import * as os from 'os';
|
|
@@ -44,7 +45,7 @@ dotenv.config({ path: GLOBAL_ENV_FILE });
|
|
|
44
45
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
45
46
|
// In dist/index.js, package.json is usually up one level in the root
|
|
46
47
|
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
47
|
-
let version = '1.3.
|
|
48
|
+
let version = '1.3.2';
|
|
48
49
|
try {
|
|
49
50
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
50
51
|
version = pkg.version;
|
|
@@ -109,7 +110,7 @@ async function runSetup(options = {}) {
|
|
|
109
110
|
}
|
|
110
111
|
const providerAnswer = await inquirer.prompt([
|
|
111
112
|
{
|
|
112
|
-
type: '
|
|
113
|
+
type: 'select',
|
|
113
114
|
name: 'provider',
|
|
114
115
|
message: 'Select your LLM provider:',
|
|
115
116
|
choices: [
|
|
@@ -121,34 +122,101 @@ async function runSetup(options = {}) {
|
|
|
121
122
|
]);
|
|
122
123
|
const provider = providerAnswer.provider;
|
|
123
124
|
const preset = resolveProvider(provider === 'custom' ? undefined : provider);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
125
|
+
// The connection part (key / Base URL / model) is asked in a loop with a
|
|
126
|
+
// live test, so typos never make it into the saved config.
|
|
127
|
+
const askConnection = async () => {
|
|
128
|
+
const defaults = {
|
|
129
|
+
apiKey: currentConfig.apiKey,
|
|
130
|
+
baseUrl: currentConfig.baseUrl || preset?.baseUrl || 'https://api.openai.com/v1',
|
|
131
|
+
model: currentConfig.model || preset?.defaultModel || 'gpt-5.6'
|
|
132
|
+
};
|
|
133
|
+
const core = await inquirer.prompt([
|
|
134
|
+
{
|
|
135
|
+
type: 'password',
|
|
136
|
+
name: 'apiKey',
|
|
137
|
+
message: defaults.apiKey
|
|
138
|
+
? `Enter API Key (Leave empty to keep ${maskSecret(defaults.apiKey)}):`
|
|
139
|
+
: `Enter API Key${preset?.apiKeyEnv ? ` (or set ${preset.apiKeyEnv} in your environment)` : ''}:`,
|
|
140
|
+
mask: '*',
|
|
141
|
+
validate: (input) => {
|
|
142
|
+
if (input.length > 0)
|
|
143
|
+
return true;
|
|
144
|
+
if (defaults.apiKey)
|
|
145
|
+
return true;
|
|
146
|
+
return 'API Key cannot be empty.';
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
type: 'input',
|
|
151
|
+
name: 'baseUrl',
|
|
152
|
+
message: 'Enter API Base URL:',
|
|
153
|
+
default: defaults.baseUrl
|
|
138
154
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
{
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
]);
|
|
156
|
+
const apiKey = core.apiKey || defaults.apiKey || '';
|
|
157
|
+
const baseUrl = normalizeBaseUrl(core.baseUrl || defaults.baseUrl);
|
|
158
|
+
// Prefer the provider's own catalog over guessing model names.
|
|
159
|
+
console.log(chalk.dim('Fetching available models...'));
|
|
160
|
+
const ids = await fetchModelIds(baseUrl, apiKey);
|
|
161
|
+
let model;
|
|
162
|
+
if (ids) {
|
|
163
|
+
console.log(chalk.dim(`Found ${ids.length} models.`));
|
|
164
|
+
const picked = await inquirer.prompt([
|
|
165
|
+
{
|
|
166
|
+
type: 'select',
|
|
167
|
+
name: 'model',
|
|
168
|
+
message: 'Select default Model:',
|
|
169
|
+
choices: [{ name: '✎ Enter manually', value: '__manual__' }, ...ids.map(id => ({ name: id, value: id }))],
|
|
170
|
+
default: ids.includes(defaults.model) ? defaults.model : undefined,
|
|
171
|
+
pageSize: 12
|
|
172
|
+
}
|
|
173
|
+
]);
|
|
174
|
+
if (picked.model === '__manual__') {
|
|
175
|
+
const manual = await inquirer.prompt([
|
|
176
|
+
{ type: 'input', name: 'model', message: 'Enter default Model:', default: defaults.model }
|
|
177
|
+
]);
|
|
178
|
+
model = manual.model;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
model = picked.model;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const manual = await inquirer.prompt([
|
|
186
|
+
{ type: 'input', name: 'model', message: 'Enter default Model (catalog unavailable):', default: defaults.model }
|
|
187
|
+
]);
|
|
188
|
+
model = manual.model;
|
|
189
|
+
}
|
|
190
|
+
return { apiKey, baseUrl, model };
|
|
191
|
+
};
|
|
192
|
+
let connection = await askConnection();
|
|
193
|
+
console.log(chalk.dim('Running connection test (sends one tiny prompt — normal provider billing applies)...'));
|
|
194
|
+
let test = await testConnection(connection.baseUrl, connection.apiKey, connection.model);
|
|
195
|
+
while (!test.ok) {
|
|
196
|
+
console.log(chalk.red(`\n✗ ${test.message}`));
|
|
197
|
+
const next = await inquirer.prompt([
|
|
198
|
+
{
|
|
199
|
+
type: 'select',
|
|
200
|
+
name: 'action',
|
|
201
|
+
message: 'Connection test failed. What next?',
|
|
202
|
+
choices: [
|
|
203
|
+
{ name: 'Re-enter API key / Base URL / model', value: 'edit' },
|
|
204
|
+
{ name: 'Test again', value: 'retry' },
|
|
205
|
+
{ name: 'Save anyway (e.g. the provider is temporarily down)', value: 'save' }
|
|
206
|
+
],
|
|
207
|
+
default: 'edit'
|
|
208
|
+
}
|
|
209
|
+
]);
|
|
210
|
+
if (next.action === 'save')
|
|
211
|
+
break;
|
|
212
|
+
if (next.action === 'edit')
|
|
213
|
+
connection = await askConnection();
|
|
214
|
+
console.log(chalk.dim('Running connection test...'));
|
|
215
|
+
test = await testConnection(connection.baseUrl, connection.apiKey, connection.model);
|
|
216
|
+
}
|
|
217
|
+
if (test.ok)
|
|
218
|
+
console.log(chalk.green('✓ Connection test passed.'));
|
|
219
|
+
const answers = await inquirer.prompt([
|
|
152
220
|
{
|
|
153
221
|
type: 'confirm',
|
|
154
222
|
name: 'configureImage',
|
|
@@ -183,7 +251,7 @@ async function runSetup(options = {}) {
|
|
|
183
251
|
}
|
|
184
252
|
]);
|
|
185
253
|
// Resolve sensitive values (Keep old if empty)
|
|
186
|
-
const finalApiKey =
|
|
254
|
+
const finalApiKey = connection.apiKey || currentConfig.apiKey;
|
|
187
255
|
let imageConfig = {
|
|
188
256
|
imageApiKey: currentConfig.imageApiKey,
|
|
189
257
|
imageBaseUrl: currentConfig.imageBaseUrl,
|
|
@@ -346,8 +414,8 @@ async function runSetup(options = {}) {
|
|
|
346
414
|
}
|
|
347
415
|
const newConfig = {
|
|
348
416
|
apiKey: finalApiKey,
|
|
349
|
-
baseUrl:
|
|
350
|
-
model:
|
|
417
|
+
baseUrl: connection.baseUrl,
|
|
418
|
+
model: connection.model,
|
|
351
419
|
provider: provider === 'custom' ? undefined : provider,
|
|
352
420
|
...imageConfig,
|
|
353
421
|
...emailConfig,
|
|
@@ -360,6 +428,8 @@ async function runSetup(options = {}) {
|
|
|
360
428
|
}
|
|
361
429
|
fs.writeFileSync(targetFile, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
|
|
362
430
|
console.log(chalk.green(`\n✅ Configuration saved to ${targetFile}`));
|
|
431
|
+
console.log(chalk.dim(` provider: ${provider === 'custom' ? 'custom' : provider} | baseUrl: ${connection.baseUrl} | model: ${connection.model}`));
|
|
432
|
+
console.log(chalk.dim(` connection test: ${test.ok ? 'passed ✓' : 'skipped (saved without a passing test)'}`));
|
|
363
433
|
console.log(chalk.cyan("You can now run 'autoclaw' to start using the agent."));
|
|
364
434
|
}
|
|
365
435
|
catch (error) {
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Pure helpers behind the setup wizard: URL normalization, a live
|
|
2
|
+
// connection test with actionable error mapping, and provider model
|
|
3
|
+
// catalog fetching. Kept free of inquirer so they are unit-testable.
|
|
4
|
+
export function normalizeBaseUrl(url) {
|
|
5
|
+
let u = String(url ?? '').trim();
|
|
6
|
+
if (u && !/^https?:\/\//i.test(u))
|
|
7
|
+
u = `https://${u}`;
|
|
8
|
+
return u.replace(/\/+$/, '');
|
|
9
|
+
}
|
|
10
|
+
// Sends one tiny real prompt through the exact endpoint the user
|
|
11
|
+
// configured, and maps failures to the field that is most likely wrong.
|
|
12
|
+
export async function testConnection(baseUrl, apiKey, model) {
|
|
13
|
+
const url = `${normalizeBaseUrl(baseUrl)}/chat/completions`;
|
|
14
|
+
let resp;
|
|
15
|
+
try {
|
|
16
|
+
resp = await fetch(url, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
19
|
+
body: JSON.stringify({ model, messages: [{ role: 'user', content: 'ping' }], stream: false }),
|
|
20
|
+
signal: AbortSignal.timeout(15000)
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
return { ok: false, kind: 'network', message: `Cannot reach ${url} (${err?.message ?? err}). Check the Base URL and your network.` };
|
|
25
|
+
}
|
|
26
|
+
if (resp.ok) {
|
|
27
|
+
return { ok: true, kind: 'ok', message: `Connection OK — ${model} responded.` };
|
|
28
|
+
}
|
|
29
|
+
const detail = (await resp.text().catch(() => '')).slice(0, 300);
|
|
30
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
31
|
+
return { ok: false, kind: 'auth', message: `API key rejected (${resp.status}). Double-check the key. ${detail}` };
|
|
32
|
+
}
|
|
33
|
+
if (resp.status === 404) {
|
|
34
|
+
return { ok: false, kind: 'not-found', message: `Endpoint not found (404). The Base URL is likely wrong. ${detail}` };
|
|
35
|
+
}
|
|
36
|
+
if (resp.status === 400) {
|
|
37
|
+
return { ok: false, kind: 'model', message: `Request rejected (400). The model name "${model}" is likely wrong for this endpoint. ${detail}` };
|
|
38
|
+
}
|
|
39
|
+
return { ok: false, kind: 'server', message: `Provider returned ${resp.status}. ${detail}` };
|
|
40
|
+
}
|
|
41
|
+
// Returns the provider's model IDs (sorted) or null when the endpoint does
|
|
42
|
+
// not offer a catalog — the wizard then falls back to manual entry.
|
|
43
|
+
export async function fetchModelIds(baseUrl, apiKey) {
|
|
44
|
+
try {
|
|
45
|
+
const resp = await fetch(`${normalizeBaseUrl(baseUrl)}/models`, {
|
|
46
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
47
|
+
signal: AbortSignal.timeout(15000)
|
|
48
|
+
});
|
|
49
|
+
if (!resp.ok)
|
|
50
|
+
return null;
|
|
51
|
+
const data = await resp.json();
|
|
52
|
+
const ids = (Array.isArray(data?.data) ? data.data : [])
|
|
53
|
+
.map((m) => m?.id)
|
|
54
|
+
.filter((id) => typeof id === 'string' && id.length > 0);
|
|
55
|
+
ids.sort();
|
|
56
|
+
return ids.length > 0 ? ids : null;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/tools/core.js
CHANGED
|
@@ -5,6 +5,9 @@ import chalk from 'chalk';
|
|
|
5
5
|
import { execShellCommand } from '../shell.js';
|
|
6
6
|
const DEFAULT_SHELL_TIMEOUT_MS = 120000;
|
|
7
7
|
const SHELL_MAX_BUFFER = 10 * 1024 * 1024;
|
|
8
|
+
// Bounded reads keep a huge file from exhausting memory or the model context;
|
|
9
|
+
// the agent-level truncation in truncate.ts applies on top of this.
|
|
10
|
+
const READ_FILE_MAX_BYTES = 1024 * 1024;
|
|
8
11
|
export const ShellTool = {
|
|
9
12
|
name: "Shell Execution",
|
|
10
13
|
definition: {
|
|
@@ -78,13 +81,30 @@ export const ReadFileTool = {
|
|
|
78
81
|
}
|
|
79
82
|
},
|
|
80
83
|
handler: async (args) => {
|
|
84
|
+
let fh;
|
|
81
85
|
try {
|
|
82
|
-
|
|
86
|
+
fh = await fs.open(args.path, 'r');
|
|
87
|
+
const buf = Buffer.alloc(READ_FILE_MAX_BYTES);
|
|
88
|
+
const { bytesRead } = await fh.read(buf, 0, READ_FILE_MAX_BYTES, 0);
|
|
89
|
+
const slice = buf.subarray(0, bytesRead);
|
|
90
|
+
// NUL bytes are the reliable tell for binary content; returning them
|
|
91
|
+
// as "utf-8" would only hand the model mojibake.
|
|
92
|
+
if (slice.includes(0)) {
|
|
93
|
+
return `Error: ${args.path} looks like a binary file (${bytesRead} bytes read). Inspect it with execute_shell_command instead (e.g. strings, xxd, file).`;
|
|
94
|
+
}
|
|
95
|
+
const content = slice.toString('utf-8');
|
|
96
|
+
if (bytesRead === READ_FILE_MAX_BYTES) {
|
|
97
|
+
const { size } = await fh.stat();
|
|
98
|
+
return `${content}\n[AutoClaw] File truncated at ${READ_FILE_MAX_BYTES} bytes (file is ${size} bytes). Use execute_shell_command to read specific ranges.`;
|
|
99
|
+
}
|
|
83
100
|
return content;
|
|
84
101
|
}
|
|
85
102
|
catch (error) {
|
|
86
103
|
return `Error reading file: ${error.message}`;
|
|
87
104
|
}
|
|
105
|
+
finally {
|
|
106
|
+
await fh?.close();
|
|
107
|
+
}
|
|
88
108
|
}
|
|
89
109
|
};
|
|
90
110
|
export const WriteFileTool = {
|
package/dist/tools/email.js
CHANGED
|
@@ -34,6 +34,9 @@ export const EmailTool = {
|
|
|
34
34
|
host: config.smtpHost,
|
|
35
35
|
port: parseInt(config.smtpPort || '587'),
|
|
36
36
|
secure: parseInt(config.smtpPort) === 465, // true for 465, false for other ports
|
|
37
|
+
connectionTimeout: 30000,
|
|
38
|
+
greetingTimeout: 30000,
|
|
39
|
+
socketTimeout: 120000,
|
|
37
40
|
auth: {
|
|
38
41
|
user: config.smtpUser,
|
|
39
42
|
pass: config.smtpPass,
|
package/dist/tools/image.js
CHANGED
|
@@ -64,7 +64,7 @@ const toolDefinition = {
|
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
66
|
async function downloadImage(url, destPath) {
|
|
67
|
-
const response = await fetch(url);
|
|
67
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(120000) });
|
|
68
68
|
if (!response.ok)
|
|
69
69
|
throw new Error(`Failed to download image: ${response.statusText}`);
|
|
70
70
|
const buffer = await response.arrayBuffer();
|
package/dist/tools/notify.js
CHANGED
|
@@ -80,7 +80,8 @@ export const NotifyTool = {
|
|
|
80
80
|
const response = await fetch(webhookUrl, {
|
|
81
81
|
method: "POST",
|
|
82
82
|
headers: { "Content-Type": "application/json" },
|
|
83
|
-
body: JSON.stringify(payload)
|
|
83
|
+
body: JSON.stringify(payload),
|
|
84
|
+
signal: AbortSignal.timeout(30000)
|
|
84
85
|
});
|
|
85
86
|
const result = await response.json();
|
|
86
87
|
// Platform specific success checks
|
package/dist/tools/search.js
CHANGED