aws-cli-agent 0.4.0 → 0.6.0
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/CHANGELOG.md +250 -2
- package/README.md +60 -13
- package/dist/agent.d.ts +12 -0
- package/dist/agent.js +151 -48
- package/dist/cli.js +36 -7
- package/dist/config.d.ts +45 -4
- package/dist/config.js +157 -47
- package/dist/errors.d.ts +79 -0
- package/dist/errors.js +96 -0
- package/dist/providers.d.ts +22 -9
- package/dist/providers.js +82 -27
- package/dist/tools/aws-cli.js +66 -10
- package/dist/tools/bash.js +3 -2
- package/dist/tools/prompt.js +10 -6
- package/package.json +14 -14
package/dist/tools/aws-cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { tool } from 'ai';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { confirm } from '@inquirer/prompts';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
+
import { FATAL_AWS_EXIT_CODES, FatalAwsCliError, UserCancelledError, wrapPrompt } from '../errors.js';
|
|
6
7
|
const READ_ONLY_VERBS = [
|
|
7
8
|
/^describe-/,
|
|
8
9
|
/^list-/,
|
|
@@ -107,14 +108,40 @@ function runCaptured(cmd, args) {
|
|
|
107
108
|
*/
|
|
108
109
|
function runInteractive(cmd, args) {
|
|
109
110
|
return new Promise((resolve, reject) => {
|
|
111
|
+
// Detach our SIGINT handler for the duration of the child's lifetime.
|
|
112
|
+
// Background: when the user presses Ctrl-C inside an SSM session, the
|
|
113
|
+
// OS delivers SIGINT to the whole foreground process group — both the
|
|
114
|
+
// aws CLI child AND this Node process. Node's default SIGINT handler
|
|
115
|
+
// would terminate us, which tears down the inherited stdin file
|
|
116
|
+
// descriptor before the aws CLI has finished its own cleanup. The
|
|
117
|
+
// result is the aws CLI seeing "input/output error" on /dev/stdin and
|
|
118
|
+
// printing a confusing error to the user.
|
|
119
|
+
//
|
|
120
|
+
// By installing a no-op SIGINT listener, we override Node's default
|
|
121
|
+
// behavior: Node sees the signal as "handled" and doesn't terminate
|
|
122
|
+
// us. The aws CLI subprocess gets the signal too (same process group)
|
|
123
|
+
// and runs its own clean shutdown — closing the SSM session politely,
|
|
124
|
+
// exiting with code 0 or 130. We then continue normally.
|
|
125
|
+
//
|
|
126
|
+
// The listener is removed in `close` so SIGINT during normal agent
|
|
127
|
+
// reasoning still aborts the run as expected.
|
|
128
|
+
const noopSigint = () => {
|
|
129
|
+
/* deliberately empty — see comment above */
|
|
130
|
+
};
|
|
131
|
+
process.on('SIGINT', noopSigint);
|
|
110
132
|
const proc = spawn(cmd, args, {
|
|
111
133
|
env: process.env,
|
|
112
|
-
stdio: 'inherit', //
|
|
134
|
+
stdio: 'inherit', // child reuses parent's stdin/stdout/stderr
|
|
135
|
+
});
|
|
136
|
+
proc.on('error', (err) => {
|
|
137
|
+
process.removeListener('SIGINT', noopSigint);
|
|
138
|
+
reject(err);
|
|
139
|
+
});
|
|
140
|
+
proc.on('close', (code) => {
|
|
141
|
+
process.removeListener('SIGINT', noopSigint);
|
|
142
|
+
// We can't observe stdout/stderr — they went to the user's terminal.
|
|
143
|
+
resolve({ stdout: '', stderr: '', code: code ?? 0 });
|
|
113
144
|
});
|
|
114
|
-
proc.on('error', reject);
|
|
115
|
-
proc.on('close', (code) =>
|
|
116
|
-
// We can't observe stdout/stderr — they went to the user's terminal.
|
|
117
|
-
resolve({ stdout: '', stderr: '', code: code ?? 0 }));
|
|
118
145
|
});
|
|
119
146
|
}
|
|
120
147
|
export function awsCliTool(opts) {
|
|
@@ -164,7 +191,7 @@ export function awsCliTool(opts) {
|
|
|
164
191
|
if (useInteractive) {
|
|
165
192
|
process.stderr.write(`${chalk.bold(' Mode: ')}${chalk.yellow('interactive')} (your terminal will be connected to the command)\n`);
|
|
166
193
|
}
|
|
167
|
-
const ok = await confirm({ message: 'Execute this command?', default: true });
|
|
194
|
+
const ok = await wrapPrompt((ctx) => confirm({ message: 'Execute this command?', default: true }, ctx));
|
|
168
195
|
if (!ok) {
|
|
169
196
|
opts.logger.warn('User declined command');
|
|
170
197
|
// Record the declined call so the agent's end-of-run logic sees
|
|
@@ -208,9 +235,18 @@ export function awsCliTool(opts) {
|
|
|
208
235
|
opts.logger.trace('stderr', stderr);
|
|
209
236
|
}
|
|
210
237
|
}
|
|
211
|
-
else if (code !== 0) {
|
|
238
|
+
else if (code !== 0 && code !== 130) {
|
|
239
|
+
// Exit 130 in interactive mode = user pressed Ctrl-C to end their
|
|
240
|
+
// SSM session, shell, port-forward, etc. That's expected, not a
|
|
241
|
+
// failure. Anything else is genuine — log it.
|
|
212
242
|
opts.logger.warn(`Interactive AWS CLI exited non-zero (${code})`);
|
|
213
243
|
}
|
|
244
|
+
// SSM sessions and other interactive AWS CLI commands return 130
|
|
245
|
+
// when the user Ctrl-Cs to end the session. That's the normal way
|
|
246
|
+
// to leave a shell — treat it as success for ok/exit purposes so we
|
|
247
|
+
// don't surface it as an error in cli.ts. The real exit code is
|
|
248
|
+
// still recorded in the audit log for accuracy.
|
|
249
|
+
const effectivelyOk = code === 0 || (useInteractive && code === 130);
|
|
214
250
|
// Audit captures whatever we have. For interactive runs stdout/stderr
|
|
215
251
|
// are empty — that's accurate, the bytes went to the terminal — and
|
|
216
252
|
// the audit entry serves as a record that "an interactive session
|
|
@@ -219,7 +255,7 @@ export function awsCliTool(opts) {
|
|
|
219
255
|
cmd: display,
|
|
220
256
|
profile,
|
|
221
257
|
exitCode: code,
|
|
222
|
-
ok:
|
|
258
|
+
ok: effectivelyOk,
|
|
223
259
|
stdout: useInteractive ? '[interactive session — output not captured]' : stdout,
|
|
224
260
|
stderr: useInteractive ? '' : stderr,
|
|
225
261
|
});
|
|
@@ -234,18 +270,33 @@ export function awsCliTool(opts) {
|
|
|
234
270
|
: stdout,
|
|
235
271
|
stderr: useInteractive ? '' : stderr,
|
|
236
272
|
exitCode: code,
|
|
237
|
-
ok:
|
|
273
|
+
ok: effectivelyOk,
|
|
238
274
|
});
|
|
239
275
|
// For the agent's context, return a clear signal that interactive
|
|
240
276
|
// mode ran so it doesn't try to parse fictional stdout.
|
|
241
277
|
if (useInteractive) {
|
|
242
278
|
return {
|
|
243
|
-
ok:
|
|
279
|
+
ok: effectivelyOk,
|
|
244
280
|
exitCode: code,
|
|
245
281
|
interactive: true,
|
|
246
282
|
note: 'Interactive session ran. Output went directly to the user\'s terminal and was not captured. Do not summarize or describe its contents.',
|
|
247
283
|
};
|
|
248
284
|
}
|
|
285
|
+
// Classify failures. Fatal exit codes (252-255) indicate the call
|
|
286
|
+
// won't succeed without external intervention — bad credentials,
|
|
287
|
+
// missing resource, malformed request, AWS service failure. We
|
|
288
|
+
// throw FatalAwsCliError (after recording the audit trail above)
|
|
289
|
+
// rather than returning a result to the model: the throw unwinds
|
|
290
|
+
// the agent loop entirely, the user sees the AWS stderr in red,
|
|
291
|
+
// and we exit 1. The model never gets a chance to retry, because
|
|
292
|
+
// these classes of error don't get better with retries.
|
|
293
|
+
//
|
|
294
|
+
// Soft failures (other non-zero exits) ARE returned to the model
|
|
295
|
+
// as ordinary results. The model may retry with a different
|
|
296
|
+
// approach. maxSteps bounds the worst case if that goes nowhere.
|
|
297
|
+
if (code !== 0 && FATAL_AWS_EXIT_CODES.has(code)) {
|
|
298
|
+
throw new FatalAwsCliError(display, code, stderr);
|
|
299
|
+
}
|
|
249
300
|
return {
|
|
250
301
|
ok: code === 0,
|
|
251
302
|
exitCode: code,
|
|
@@ -254,6 +305,11 @@ export function awsCliTool(opts) {
|
|
|
254
305
|
};
|
|
255
306
|
}
|
|
256
307
|
catch (err) {
|
|
308
|
+
// FatalAwsCliError is our own signal — propagate it cleanly.
|
|
309
|
+
// UserCancelledError must propagate too (Ctrl-C at the approval
|
|
310
|
+
// prompt) or it'd get swallowed into a spawn-failure log entry.
|
|
311
|
+
if (err instanceof FatalAwsCliError || err instanceof UserCancelledError)
|
|
312
|
+
throw err;
|
|
257
313
|
const msg = err instanceof Error ? err.message : String(err);
|
|
258
314
|
opts.logger.error('Failed to spawn aws CLI', msg);
|
|
259
315
|
opts.audit.logCommand({
|
package/dist/tools/bash.js
CHANGED
|
@@ -7,6 +7,7 @@ import { z } from 'zod';
|
|
|
7
7
|
import { select } from '@inquirer/prompts';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { DEFAULT_SCRIPT_FOLDER } from '../paths.js';
|
|
10
|
+
import { wrapPrompt } from '../errors.js';
|
|
10
11
|
function runProcess(cmd, args) {
|
|
11
12
|
return new Promise((resolve, reject) => {
|
|
12
13
|
const proc = spawn(cmd, args, { env: process.env });
|
|
@@ -74,7 +75,7 @@ export function bashScriptTool(opts) {
|
|
|
74
75
|
// — auto-approving them would defeat a primary safety boundary. The
|
|
75
76
|
// autoApprove flag remains in effect for individual aws CLI commands
|
|
76
77
|
// (where read-only is a meaningful and enforceable category).
|
|
77
|
-
const action = await select({
|
|
78
|
+
const action = await wrapPrompt((ctx) => select({
|
|
78
79
|
message: 'What would you like to do with this script?',
|
|
79
80
|
choices: [
|
|
80
81
|
{ value: 'execute', name: 'Execute now' },
|
|
@@ -82,7 +83,7 @@ export function bashScriptTool(opts) {
|
|
|
82
83
|
{ value: 'cancel', name: 'Cancel' },
|
|
83
84
|
],
|
|
84
85
|
default: 'execute',
|
|
85
|
-
});
|
|
86
|
+
}, ctx));
|
|
86
87
|
if (action === 'cancel') {
|
|
87
88
|
opts.logger.warn('User cancelled script');
|
|
88
89
|
// Record the cancelled call so the agent's end-of-run logic sees
|
package/dist/tools/prompt.js
CHANGED
|
@@ -2,6 +2,7 @@ import { tool } from 'ai';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { confirm, input, password, select } from '@inquirer/prompts';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
+
import { wrapPrompt } from '../errors.js';
|
|
5
6
|
/**
|
|
6
7
|
* Schema for a single question. Used both by `prompt_user` directly (single
|
|
7
8
|
* question per call) and `prompt_user_multi` (batch of questions in one call).
|
|
@@ -44,28 +45,31 @@ async function askOne(q, logger) {
|
|
|
44
45
|
if (!q.choices || q.choices.length === 0) {
|
|
45
46
|
throw new Error('kind="choice" requires non-empty `choices`.');
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
+
// Capture narrowed value: TS loses the q.choices !== undefined
|
|
49
|
+
// narrowing across the closure boundary below.
|
|
50
|
+
const choices = q.choices;
|
|
51
|
+
const answer = await wrapPrompt((ctx) => select({
|
|
48
52
|
message: q.message,
|
|
49
|
-
choices:
|
|
53
|
+
choices: choices.map((c) => ({ value: c, name: c })),
|
|
50
54
|
default: q.defaultValue,
|
|
51
|
-
});
|
|
55
|
+
}, ctx));
|
|
52
56
|
return answer;
|
|
53
57
|
}
|
|
54
58
|
case 'confirm': {
|
|
55
59
|
const def = (q.defaultValue ?? 'yes').toLowerCase().startsWith('y');
|
|
56
|
-
const answer = await confirm({ message: q.message, default: def });
|
|
60
|
+
const answer = await wrapPrompt((ctx) => confirm({ message: q.message, default: def }, ctx));
|
|
57
61
|
return answer ? 'yes' : 'no';
|
|
58
62
|
}
|
|
59
63
|
case 'secret': {
|
|
60
64
|
// Inquirer's password prompt masks input. Used for short secrets like
|
|
61
65
|
// MFA codes; long-lived AWS credentials should always come from the
|
|
62
66
|
// user's profile, not be typed here.
|
|
63
|
-
const answer = await password({ message: q.message, mask: '*' });
|
|
67
|
+
const answer = await wrapPrompt((ctx) => password({ message: q.message, mask: '*' }, ctx));
|
|
64
68
|
return answer;
|
|
65
69
|
}
|
|
66
70
|
case 'text':
|
|
67
71
|
default: {
|
|
68
|
-
const answer = await input({ message: q.message, default: q.defaultValue });
|
|
72
|
+
const answer = await wrapPrompt((ctx) => input({ message: q.message, default: q.defaultValue }, ctx));
|
|
69
73
|
return answer;
|
|
70
74
|
}
|
|
71
75
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aws-cli-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Agentic AI assistant that turns natural-language requests into AWS CLI commands and runs them locally.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -51,23 +51,23 @@
|
|
|
51
51
|
},
|
|
52
52
|
"homepage": "https://github.com/trstnk/aws-cli-agent#readme",
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@ai-sdk/amazon-bedrock": "^4.0.
|
|
55
|
-
"@ai-sdk/anthropic": "^3.0.
|
|
56
|
-
"@ai-sdk/google": "^3.0.
|
|
57
|
-
"@ai-sdk/openai": "^3.0.
|
|
58
|
-
"@aws-sdk/credential-providers": "^3.
|
|
59
|
-
"@inquirer/prompts": "^
|
|
60
|
-
"ai": "^6.0.
|
|
61
|
-
"chalk": "^5.
|
|
62
|
-
"commander": "^
|
|
54
|
+
"@ai-sdk/amazon-bedrock": "^4.0.111",
|
|
55
|
+
"@ai-sdk/anthropic": "^3.0.81",
|
|
56
|
+
"@ai-sdk/google": "^3.0.80",
|
|
57
|
+
"@ai-sdk/openai": "^3.0.67",
|
|
58
|
+
"@aws-sdk/credential-providers": "^3.1057.0",
|
|
59
|
+
"@inquirer/prompts": "^8.5.1",
|
|
60
|
+
"ai": "^6.0.193",
|
|
61
|
+
"chalk": "^5.6.2",
|
|
62
|
+
"commander": "^15.0.0",
|
|
63
63
|
"zod": "^4.4.3"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@eslint/js": "^10.0.1",
|
|
67
|
-
"@types/node": "^25.
|
|
68
|
-
"eslint": "^10.4.
|
|
69
|
-
"tsx": "^4.22.
|
|
67
|
+
"@types/node": "^25.9.1",
|
|
68
|
+
"eslint": "^10.4.1",
|
|
69
|
+
"tsx": "^4.22.3",
|
|
70
70
|
"typescript": "^6.0.3",
|
|
71
|
-
"typescript-eslint": "^8.
|
|
71
|
+
"typescript-eslint": "^8.60.0"
|
|
72
72
|
}
|
|
73
73
|
}
|