aws-cli-agent 0.4.0 → 0.5.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 +200 -2
- package/README.md +7 -1
- package/dist/agent.d.ts +12 -0
- package/dist/agent.js +146 -45
- package/dist/cli.js +22 -6
- package/dist/errors.d.ts +57 -0
- package/dist/errors.js +76 -0
- package/dist/tools/aws-cli.js +35 -5
- package/dist/tools/bash.js +3 -2
- package/dist/tools/prompt.js +6 -5
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,206 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
|
5
5
|
versioning follows [SemVer](https://semver.org/).
|
|
6
6
|
|
|
7
|
-
## [0.
|
|
7
|
+
## [0.5.0] - 2026-05-19
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
11
|
-
-
|
|
11
|
+
- **Graceful error handling for AWS CLI failures.** AWS CLI exit codes
|
|
12
|
+
252–255 (parse error, missing credentials, client error, server error)
|
|
13
|
+
are now classified as fatal and abort the agent loop immediately rather
|
|
14
|
+
than being fed back to the model for retry. The user sees the AWS
|
|
15
|
+
stderr printed verbatim in red; the process exits 1. Other non-zero
|
|
16
|
+
exits remain soft failures — the model can decide whether to retry,
|
|
17
|
+
bounded by `maxSteps` as before.
|
|
18
|
+
- **Ctrl-C handling.** Pressing Ctrl-C at any agent-driven prompt
|
|
19
|
+
(approval prompts, agent-asked questions, the bash script's
|
|
20
|
+
execute/save/cancel dialog) now exits cleanly with a "cancelled by
|
|
21
|
+
user" message on stderr and exit code 130 (SIGINT convention). No
|
|
22
|
+
stack trace, no red error, no "ran N commands" footer.
|
|
23
|
+
- **SSM-session Ctrl-C silenced.** When you Ctrl-C to end an interactive
|
|
24
|
+
AWS CLI session (SSM Session Manager shells, port-forwards, etc.),
|
|
25
|
+
exit code 130 is treated as a clean termination instead of an error.
|
|
26
|
+
The audit log still records the real exit code for accuracy.
|
|
27
|
+
- **`endReason` field on `RunResult`.** Internal API used by cli.ts to
|
|
28
|
+
pick the right exit code: `completed` (0), `cancelled` (130), or
|
|
29
|
+
`fatal` (1).
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **The "ran N commands" footer is now verbose-only.** Previously printed
|
|
34
|
+
on every multi-command run; now requires `--verbose` / `-v` to surface.
|
|
35
|
+
With verbose off, nothing aca generates reaches the terminal — only
|
|
36
|
+
the AWS CLI's verbatim output does, matching the README's promise.
|
|
37
|
+
|
|
38
|
+
## [0.4.0] - 2026-05-18
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- **Dependency upgrades.** Vercel AI SDK v4 → v6, zod v3 → v4, TypeScript
|
|
43
|
+
v5 → v6, ESLint v9 → v10, `@types/node` v22 → v25, and all `@ai-sdk/*`
|
|
44
|
+
provider packages to their v6-compatible majors (`@ai-sdk/anthropic`@3,
|
|
45
|
+
`@ai-sdk/openai`@3, `@ai-sdk/google`@3, `@ai-sdk/amazon-bedrock`@4).
|
|
46
|
+
Required code changes:
|
|
47
|
+
- `generateText({ maxSteps })` → `generateText({ stopWhen: stepCountIs(n) })`
|
|
48
|
+
- Tool definition field `parameters:` → `inputSchema:`
|
|
49
|
+
- Tool call payload `args` → `input`
|
|
50
|
+
- Usage fields `promptTokens` / `completionTokens` →
|
|
51
|
+
`inputTokens` / `outputTokens` (the data we write to `usage.log` keeps
|
|
52
|
+
the legacy names — they're a stable public interface, just remapped at
|
|
53
|
+
extraction time).
|
|
54
|
+
- `createOpenAI({ compatibility: 'strict' })` removed; the option no longer
|
|
55
|
+
exists.
|
|
56
|
+
- Zod v4 `.default({})` on object schemas now requires the fully-typed
|
|
57
|
+
default value; updated `LoggingSchema` and `autoApprove` defaults.
|
|
58
|
+
- Step events from `onStepFinish` dropped the `stepType` field; the debug
|
|
59
|
+
log now only mentions `finishReason`.
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- **Interactive AWS CLI commands now work.** Previously, commands like
|
|
64
|
+
`aws ssm start-session` (interactive shells), port-forwarding sessions, and
|
|
65
|
+
log tails with `--follow` appeared to hang — the child process's stdout was
|
|
66
|
+
being captured into a string for the agent's context, and the child's stdin
|
|
67
|
+
was never connected to the user's terminal. Now the host detects common
|
|
68
|
+
interactive patterns and uses `stdio: 'inherit'` for those commands, so the
|
|
69
|
+
user's terminal connects directly to the AWS CLI subprocess.
|
|
70
|
+
- **General log no longer echoes to the console.** Previously, the operational
|
|
71
|
+
`Logger` wrote both to `general.log` *and* to stderr at every level above
|
|
72
|
+
the threshold — meaning `--log-level debug` would spam debug lines into the
|
|
73
|
+
user's terminal. Now `Logger` is strictly file-only; the only things that
|
|
74
|
+
reach the console are (a) the AWS CLI's verbatim stdout, (b) approval
|
|
75
|
+
prompts, (c) error summaries, and (d) reasoning steps when `verbose` is
|
|
76
|
+
on. To watch operational logs live: `tail -f
|
|
77
|
+
~/.local/state/aws-cli-agent/general.log`.
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
- **`--interactive` / `-i` CLI flag** to force every AWS CLI command in a run
|
|
82
|
+
to inherit the user's terminal. Useful as an escape hatch for commands not
|
|
83
|
+
in the auto-detect list (`ssm start-session`, `ssm start-session` with
|
|
84
|
+
port-forward documents, `ecs execute-command`, `logs tail --follow`).
|
|
85
|
+
- **`interactive` parameter on `execute_aws_command` tool.** Lets the agent
|
|
86
|
+
explicitly mark a command as interactive when it knows the command needs
|
|
87
|
+
a TTY. For interactive runs, the agent receives a "do not summarize"
|
|
88
|
+
signal instead of stdout.
|
|
89
|
+
- **Auto-approve never applies to interactive commands.** Handing your
|
|
90
|
+
terminal to a subprocess is a meaningful event; it always prompts.
|
|
91
|
+
|
|
92
|
+
- **Prompt caching** for Anthropic and Bedrock providers (`caching: true` by
|
|
93
|
+
default). Marks the system prompt + tool definitions as cacheable; cache
|
|
94
|
+
reads cost ~10% of normal input tokens on these providers. OpenAI
|
|
95
|
+
auto-caches without our involvement; Google Gemini isn't supported yet.
|
|
96
|
+
Cache hit/miss tokens are recorded in `usage.log` as `cacheReadTokens` and
|
|
97
|
+
`cacheWriteTokens`. Typical cost reduction: ~60% off the input bill for
|
|
98
|
+
frequent users.
|
|
99
|
+
- **Usage log** — `usage.log` (JSONL) records token totals per `aca`
|
|
100
|
+
invocation: timestamp, provider, model, steps, prompt/completion/total
|
|
101
|
+
tokens. Enable/disable via `logging.usageLog` (default `true`). Sum the
|
|
102
|
+
day's tokens with `cat usage.log | jq -s 'map(.totalTokens) | add'`.
|
|
103
|
+
- **Interactive prompting** during the reasoning process. The `prompt_user`
|
|
104
|
+
tool now supports four question kinds: `text` (free-form), `choice` (pick
|
|
105
|
+
one from a finite list), `confirm` (yes/no), and `secret` (hidden input
|
|
106
|
+
for short secrets like MFA codes). New `prompt_user_multi` tool batches
|
|
107
|
+
several related questions into a single round so the agent doesn't need
|
|
108
|
+
multiple model round-trips to gather setup data.
|
|
109
|
+
- **Sharpened system prompt** with explicit anti-guessing rules and worked
|
|
110
|
+
examples of when to ask vs. when to discover. The agent is much more
|
|
111
|
+
likely to stop and ask when it isn't certain rather than picking a
|
|
112
|
+
plausible answer and acting on it.
|
|
113
|
+
|
|
114
|
+
## [0.3.0] - 2026-05-15
|
|
115
|
+
|
|
116
|
+
### Changed
|
|
117
|
+
|
|
118
|
+
- **Renamed** package from `ai-aws` to `aws-cli-agent`. Short CLI name is `aca`;
|
|
119
|
+
the long name `aws-cli-agent` works too. Install with
|
|
120
|
+
`npm install -g aws-cli-agent`.
|
|
121
|
+
- **Restructured logging config.** Replaced top-level `logLevel`, `audit.enabled`,
|
|
122
|
+
and `reasoning.enabled` with a nested `logging` object:
|
|
123
|
+
```json
|
|
124
|
+
"logging": { "level": "error", "auditLog": true, "reasoningLog": false }
|
|
125
|
+
```
|
|
126
|
+
Defaults are now: level `error` (was `info`), audit on (unchanged), reasoning
|
|
127
|
+
log **off** (was on).
|
|
128
|
+
- **Renamed general log file** from `ai-aws.log` to `general.log`.
|
|
129
|
+
- **`--verbose` is now reasoning-only.** Previously also bumped log level to
|
|
130
|
+
debug; now controls only whether reasoning is echoed to the console. Use
|
|
131
|
+
`--log-level debug` separately if you want a noisier general log.
|
|
132
|
+
- **Restructured Bedrock config** into a nested `bedrock` object:
|
|
133
|
+
```json
|
|
134
|
+
"bedrock": { "region": "us-east-1", "profile": "shared-services" }
|
|
135
|
+
```
|
|
136
|
+
Replaces the old top-level `bedrockRegion` / `bedrockProfile`.
|
|
137
|
+
|
|
138
|
+
### Added
|
|
139
|
+
|
|
140
|
+
- **`defaultRegion`** config and `--region` CLI flag. The configured region
|
|
141
|
+
is auto-appended as `--region` to every AWS CLI call the agent makes —
|
|
142
|
+
unless the agent itself specified a region, in which case its choice wins.
|
|
143
|
+
- **Bash script "save to disk" option.** When the agent generates a script,
|
|
144
|
+
the user now sees a three-way prompt: Execute / Save to disk / Cancel.
|
|
145
|
+
The save path is shown inline so you know exactly where the file lands.
|
|
146
|
+
Folder is configurable via `scriptFolder`; default is
|
|
147
|
+
`$XDG_DATA_HOME/aws-cli-agent/scripts`.
|
|
148
|
+
- **Two npm-installable binary names**: `aws-cli-agent` and `aca` (same binary).
|
|
149
|
+
- **GitHub Actions CI** (lint, typecheck, build, smoke test on Node 20 & 22).
|
|
150
|
+
- **GitHub Actions Release** workflow (publishes to npm on tag push or release
|
|
151
|
+
publication, with provenance attestation).
|
|
152
|
+
- **Dependabot** config for npm and GitHub Actions.
|
|
153
|
+
- **Smoke test** script (`npm test`) that exercises the basic CLI surface
|
|
154
|
+
without needing cloud credentials.
|
|
155
|
+
|
|
156
|
+
### Removed
|
|
157
|
+
|
|
158
|
+
- **`--quiet` / `-q` flag.** Use `--log-level error` (or `silent`) instead.
|
|
159
|
+
- **Top-level config keys** `logLevel`, `audit`, `reasoning`, `bedrockRegion`,
|
|
160
|
+
`bedrockProfile`. See "Changed" above.
|
|
161
|
+
- **`autoApprove` no longer applies to bash scripts.** Scripts always prompt
|
|
162
|
+
(Execute / Save / Cancel). The flag still skips approval for individual
|
|
163
|
+
AWS CLI calls.
|
|
164
|
+
|
|
165
|
+
### Migration notes
|
|
166
|
+
|
|
167
|
+
The old `ai-aws` config at `~/.config/ai-aws/config.json` is not read or
|
|
168
|
+
migrated. Run `aca config` to write a fresh default at the new path. Translate
|
|
169
|
+
old → new keys:
|
|
170
|
+
|
|
171
|
+
| Old | New |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `logLevel` | `logging.level` |
|
|
174
|
+
| `audit.enabled` | `logging.auditLog` |
|
|
175
|
+
| `reasoning.enabled` | `logging.reasoningLog` |
|
|
176
|
+
| `bedrockRegion` | `bedrock.region` |
|
|
177
|
+
| `bedrockProfile` | `bedrock.profile` |
|
|
178
|
+
|
|
179
|
+
History at the old `~/.local/state/ai-aws/` location won't be picked up.
|
|
180
|
+
If you want to keep it: `mv ~/.local/state/ai-aws ~/.local/state/aws-cli-agent`.
|
|
181
|
+
|
|
182
|
+
## [0.2.0] - 2026-05-14
|
|
183
|
+
|
|
184
|
+
### Added
|
|
185
|
+
|
|
186
|
+
- Amazon Bedrock as a provider option via `@ai-sdk/amazon-bedrock`. Uses the
|
|
187
|
+
standard AWS credential chain; no API key required. Configurable via
|
|
188
|
+
optional `bedrockRegion` and `bedrockProfile` (since superseded by nested
|
|
189
|
+
`bedrock` in 0.3.0).
|
|
190
|
+
- Audit log: append-only JSONL of every executed command/script with full
|
|
191
|
+
stdout/stderr/exit code. Bash scripts also log full source.
|
|
192
|
+
- Reasoning log: text record of agent reasoning steps and tool calls.
|
|
193
|
+
- ESLint 9 with flat config (`npm run lint`, `npm run lint:fix`).
|
|
194
|
+
|
|
195
|
+
### Changed
|
|
196
|
+
|
|
197
|
+
- Output policy: stdout is reserved for the verbatim AWS CLI output. The agent
|
|
198
|
+
cannot rewrite or summarize results. Pipe to `jq`, `wc`, etc. like you would
|
|
199
|
+
with the AWS CLI directly.
|
|
200
|
+
- Moved `history.jsonl` from `$XDG_DATA_HOME` to `$XDG_STATE_HOME` alongside
|
|
201
|
+
the logs.
|
|
202
|
+
|
|
203
|
+
## [0.1.0] - 2026-05-14
|
|
204
|
+
|
|
205
|
+
### Added
|
|
206
|
+
|
|
207
|
+
- Initial release. Agentic AWS CLI assistant with multi-step tool calling
|
|
208
|
+
(Vercel AI SDK), local-only state, XDG-compliant paths, configurable
|
|
209
|
+
providers (Anthropic / OpenAI / Google), per-command approval prompts.
|
package/README.md
CHANGED
|
@@ -33,6 +33,12 @@ The first example is interactive — the agent runs a read-only `describe-instan
|
|
|
33
33
|
- **Audit log is your friend.** Every executed command — including its stdout, stderr, and exit code — lands in `audit.log` (JSONL). If you ever need to reconstruct what happened, it's all there. Don't disable `logging.auditLog` unless you have a specific reason.
|
|
34
34
|
- **No warranty.** **You use this agent at your own risk.** The authors are not responsible for unintended AWS API calls, deleted resources, exceeded budgets, or any other damage caused by using this tool. If you wouldn't run `aws` commands blindly from a script you found in someone's gist, don't run `aca` blindly either.
|
|
35
35
|
|
|
36
|
+
## Trademark & affiliation
|
|
37
|
+
|
|
38
|
+
`aws-cli-agent` (`aca`) is an independent project, not affiliated with or
|
|
39
|
+
endorsed by Amazon Web Services. "AWS" and "Amazon Web Services" are
|
|
40
|
+
trademarks of Amazon.com, Inc.
|
|
41
|
+
|
|
36
42
|
## Installation
|
|
37
43
|
|
|
38
44
|
```bash
|
|
@@ -370,4 +376,4 @@ Without this rule, the approval prompts and reasoning lines would land in the ne
|
|
|
370
376
|
|
|
371
377
|
## License
|
|
372
378
|
|
|
373
|
-
MIT
|
|
379
|
+
MIT
|
package/dist/agent.d.ts
CHANGED
|
@@ -28,6 +28,18 @@ export type RunResult = {
|
|
|
28
28
|
finalError: string | null;
|
|
29
29
|
/** Did the last execute_* call run successfully? */
|
|
30
30
|
ranCommand: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* How the run ended. cli.ts uses this to pick the exit code and decide
|
|
33
|
+
* whether to print the AWS stderr as a red error:
|
|
34
|
+
* - 'completed' — normal end, model stopped on its own (exit 0)
|
|
35
|
+
* - 'cancelled' — Ctrl-C at a prompt; cli.ts prints "cancelled by
|
|
36
|
+
* user" on stderr and exits 130 (the canonical SIGINT
|
|
37
|
+
* exit code)
|
|
38
|
+
* - 'fatal' — AWS CLI returned an unrecoverable exit code
|
|
39
|
+
* (252-255); finalError carries the stderr, cli.ts
|
|
40
|
+
* prints it in red and exits 1
|
|
41
|
+
*/
|
|
42
|
+
endReason: 'completed' | 'cancelled' | 'fatal';
|
|
31
43
|
};
|
|
32
44
|
export declare function runAgent(opts: {
|
|
33
45
|
input: string;
|
package/dist/agent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { streamText, stepCountIs } from 'ai';
|
|
2
2
|
import { createModel } from './providers.js';
|
|
3
3
|
import { createTools } from './tools/index.js';
|
|
4
|
+
import { FatalAwsCliError, UserCancelledError } from './errors.js';
|
|
4
5
|
const SYSTEM_PROMPT = `You are aws-cli-agent (aca), an agentic assistant that translates natural-language requests into AWS CLI commands and executes them locally on the user's machine.
|
|
5
6
|
|
|
6
7
|
Capabilities (via tools):
|
|
@@ -37,7 +38,8 @@ Operating rules:
|
|
|
37
38
|
8. Interactive commands: some AWS CLI commands require a real terminal — SSM Session Manager shells (\`ssm start-session\`), port-forwarding sessions (the same command with --document-name AWS-StartPortForwardingSession*), ECS Exec (\`ecs execute-command\`), log tails with --follow. For these, set \`interactive: true\` on the execute_aws_command call. The host will connect the user's terminal directly to the command and you will receive no stdout — DO NOT try to summarize or describe the output afterwards, since you can't see it. Common patterns auto-detect, but setting the flag explicitly is safer.
|
|
38
39
|
9. The final action of a successful run MUST be either execute_aws_command (the user-requested action) or execute_bash_script. If the user cancels via prompt_user, stop gracefully and explain in one sentence.
|
|
39
40
|
10. NEVER include credentials, API keys, secrets, or session tokens in commands or scripts. AWS credentials come from the user's existing profile.
|
|
40
|
-
11.
|
|
41
|
+
11. Handling AWS CLI errors: if execute_aws_command returns a result with \`ok: false\` (and a non-zero exitCode), you may retry ONCE with a different approach if it's clearly worth trying — wrong region, wrong profile, missing flag, fixable typo. Don't loop trying minor variations. The host caps total run length via maxSteps; respect it. Note: unrecoverable errors (auth failure, missing credentials, permission denied, malformed request, AWS service errors) terminate the run before you'd see them, so you don't need to handle those cases — they're handled for you.
|
|
42
|
+
12. Keep your reasoning concise — one or two sentences per step. DO NOT summarize, restate, reformat, or describe the output of the AWS CLI. The CLI's stdout is shown to the user directly by the host program. Your only post-execution job is to stop. If anything went wrong, say so briefly; if it succeeded, you may stop without further commentary.`;
|
|
41
43
|
export async function runAgent(opts) {
|
|
42
44
|
const { input, config, logger, history, audit, reasoning, usage } = opts;
|
|
43
45
|
const executions = [];
|
|
@@ -144,61 +146,159 @@ export async function runAgent(opts) {
|
|
|
144
146
|
// Two execution sites collaborate to print one step:
|
|
145
147
|
// 1. text-end (here) → reasoning text line
|
|
146
148
|
// 2. onToolCallStart (callback above) → tool: line, then execute()
|
|
147
|
-
for await
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!reasoningEchoed) {
|
|
163
|
-
reasoning.echoReasoning(stepCounter, currentReasoning);
|
|
164
|
-
reasoningEchoed = true;
|
|
149
|
+
// Terminal state for the run. The for-await loop transitions us out of
|
|
150
|
+
// 'completed' (the default) into 'cancelled' on Ctrl-C, or 'fatal' on
|
|
151
|
+
// an unrecoverable AWS CLI failure. cli.ts uses endReason to pick the
|
|
152
|
+
// exit code and the user-facing message.
|
|
153
|
+
let endReason = 'completed';
|
|
154
|
+
try {
|
|
155
|
+
for await (const part of result.fullStream) {
|
|
156
|
+
switch (part.type) {
|
|
157
|
+
case 'start-step': {
|
|
158
|
+
stepCounter += 1;
|
|
159
|
+
toolCallStepNumber = stepCounter; // visible to onToolCallStart
|
|
160
|
+
currentReasoning = '';
|
|
161
|
+
currentToolCalls = [];
|
|
162
|
+
reasoningEchoed = false;
|
|
163
|
+
break;
|
|
165
164
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
reasoning.echoReasoning(stepCounter, currentReasoning);
|
|
177
|
-
reasoningEchoed = true;
|
|
165
|
+
case 'text-delta': {
|
|
166
|
+
currentReasoning += part.text;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case 'text-end': {
|
|
170
|
+
if (!reasoningEchoed) {
|
|
171
|
+
reasoning.echoReasoning(stepCounter, currentReasoning);
|
|
172
|
+
reasoningEchoed = true;
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
178
175
|
}
|
|
179
|
-
|
|
176
|
+
case 'tool-call': {
|
|
177
|
+
// Backup echo path: if text-end didn't fire (provider variant or
|
|
178
|
+
// text-less step), echo whatever reasoning we have when we see
|
|
179
|
+
// tool-call. The tool-call LINE itself is NOT printed here — it's
|
|
180
|
+
// printed by experimental_onToolCallStart, which fires
|
|
181
|
+
// synchronously before execute() and guarantees ordering above
|
|
182
|
+
// any approval prompt.
|
|
183
|
+
if (!reasoningEchoed) {
|
|
184
|
+
reasoning.echoReasoning(stepCounter, currentReasoning);
|
|
185
|
+
reasoningEchoed = true;
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case 'tool-error': {
|
|
190
|
+
// The SDK catches errors thrown from tool.execute() and emits
|
|
191
|
+
// them as tool-error events instead of rejecting the stream. So
|
|
192
|
+
// we inspect every tool-error for our sentinels:
|
|
193
|
+
//
|
|
194
|
+
// - UserCancelledError → throw out of the loop so the outer
|
|
195
|
+
// catch propagates it to cli.ts for "cancelled by user" + exit 130.
|
|
196
|
+
// - FatalAwsCliError → set endReason='fatal' and stop iterating.
|
|
197
|
+
// The failed call has already been recorded in executions[]
|
|
198
|
+
// by the tool (audit + record fire before the throw), so
|
|
199
|
+
// finalError further down will pick up the stderr naturally.
|
|
200
|
+
// - Anything else: ignore. Soft failures shouldn't be thrown
|
|
201
|
+
// (tools return them as results), and any other thrown error
|
|
202
|
+
// is treated as a tool-level failure the model can decide
|
|
203
|
+
// how to handle.
|
|
204
|
+
if (part.error instanceof UserCancelledError) {
|
|
205
|
+
throw part.error;
|
|
206
|
+
}
|
|
207
|
+
if (part.error instanceof FatalAwsCliError) {
|
|
208
|
+
endReason = 'fatal';
|
|
209
|
+
logger.warn(`Run ended on fatal AWS CLI error (exit ${part.error.exitCode}).`);
|
|
210
|
+
// Flush this step's reasoning to the file log; the tool-call
|
|
211
|
+
// event for this step already fired, so currentToolCalls is
|
|
212
|
+
// populated. We need to break out cleanly without waiting
|
|
213
|
+
// for finish-step (the SDK may still emit it, may not).
|
|
214
|
+
reasoning.logStepToFile({
|
|
215
|
+
step: stepCounter,
|
|
216
|
+
reasoning: currentReasoning,
|
|
217
|
+
toolCalls: currentToolCalls,
|
|
218
|
+
finishReason: 'fatal-error',
|
|
219
|
+
});
|
|
220
|
+
// Stop processing the stream. We don't break out of the
|
|
221
|
+
// for-await directly because we want to drain remaining events
|
|
222
|
+
// for the SDK's internal cleanup; but we set a flag so we
|
|
223
|
+
// don't process them.
|
|
224
|
+
// Simplest: just let the loop continue. finish-step / finish
|
|
225
|
+
// events will pass through harmlessly.
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case 'finish-step': {
|
|
230
|
+
// After a fatal tool-error, finish-step still arrives for the
|
|
231
|
+
// same step. The reasoning was already flushed in the tool-error
|
|
232
|
+
// handler — don't double-flush. For normal steps, this is the
|
|
233
|
+
// path that flushes.
|
|
234
|
+
if (endReason !== 'fatal') {
|
|
235
|
+
reasoning.logStepToFile({
|
|
236
|
+
step: stepCounter,
|
|
237
|
+
reasoning: currentReasoning,
|
|
238
|
+
toolCalls: currentToolCalls,
|
|
239
|
+
finishReason: part.finishReason,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
logger.debug(`Step ${stepCounter} finished (finishReason=${part.finishReason})`);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
// Other event types (reasoning-delta for thinking-models,
|
|
246
|
+
// tool-input-delta, source, file, raw, etc.) are ignored —
|
|
247
|
+
// fullStream is forward-compatible.
|
|
180
248
|
}
|
|
181
|
-
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
// The for-await loop throws when we re-throw UserCancelledError above.
|
|
253
|
+
// It can also throw on genuine SDK / provider failures. We distinguish:
|
|
254
|
+
if (err instanceof UserCancelledError) {
|
|
255
|
+
// No endReason='cancelled' assignment here: we throw immediately
|
|
256
|
+
// and the post-stream code in this function never runs. cli.ts is
|
|
257
|
+
// the one that recognizes UserCancelledError and exits 130 — it
|
|
258
|
+
// doesn't need RunResult.endReason for that.
|
|
259
|
+
if (currentReasoning.trim().length > 0 || currentToolCalls.length > 0) {
|
|
182
260
|
reasoning.logStepToFile({
|
|
183
261
|
step: stepCounter,
|
|
184
262
|
reasoning: currentReasoning,
|
|
185
263
|
toolCalls: currentToolCalls,
|
|
186
|
-
finishReason:
|
|
264
|
+
finishReason: 'cancelled',
|
|
187
265
|
});
|
|
188
|
-
logger.debug(`Step ${stepCounter} finished (finishReason=${part.finishReason})`);
|
|
189
|
-
break;
|
|
190
266
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
// fullStream is forward-compatible.
|
|
267
|
+
logger.info('Run cancelled by user.');
|
|
268
|
+
throw err;
|
|
194
269
|
}
|
|
270
|
+
// Genuine bug or provider failure. Let it bubble.
|
|
271
|
+
throw err;
|
|
272
|
+
}
|
|
273
|
+
// After the stream completes (normally OR via FatalAwsCliError), pull
|
|
274
|
+
// the post-stream promises. Most runs reach here with all three already
|
|
275
|
+
// resolved (the stream completion is the signal). But when we caught a
|
|
276
|
+
// FatalAwsCliError mid-stream, the SDK may have left these in a rejected
|
|
277
|
+
// state — the stream didn't naturally complete. Defensive try/await
|
|
278
|
+
// around each so we degrade gracefully: a partial RunResult with
|
|
279
|
+
// whatever usage we got from steps that did complete is better than
|
|
280
|
+
// crashing on a downstream `await` and losing the failure context.
|
|
281
|
+
let finalText = '';
|
|
282
|
+
let finalSteps = [];
|
|
283
|
+
let totalUsage;
|
|
284
|
+
try {
|
|
285
|
+
finalText = await result.text;
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
logger.debug('result.text rejected (expected after fatal/cancel)', err);
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
finalSteps = await result.steps;
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
logger.debug('result.steps rejected (expected after fatal/cancel)', err);
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
totalUsage = await result.totalUsage;
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
logger.debug('result.totalUsage rejected (expected after fatal/cancel)', err);
|
|
195
301
|
}
|
|
196
|
-
// Wait for all the post-stream promises to resolve. They're already
|
|
197
|
-
// ready by the time fullStream finishes (the stream completion is the
|
|
198
|
-
// signal), so these awaits are effectively synchronous.
|
|
199
|
-
const finalText = await result.text;
|
|
200
|
-
const finalSteps = await result.steps;
|
|
201
|
-
const totalUsage = await result.totalUsage;
|
|
202
302
|
logger.info(`Agent finished after ${finalSteps.length} step(s)`);
|
|
203
303
|
logger.debug('Final text', finalText);
|
|
204
304
|
// Token usage for this invocation.
|
|
@@ -278,6 +378,7 @@ export async function runAgent(opts) {
|
|
|
278
378
|
finalOutput,
|
|
279
379
|
finalError,
|
|
280
380
|
ranCommand,
|
|
381
|
+
endReason,
|
|
281
382
|
};
|
|
282
383
|
}
|
|
283
384
|
/**
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,8 @@ import { UsageLogger } from './usage.js';
|
|
|
8
8
|
import { History } from './history.js';
|
|
9
9
|
import { runAgent } from './agent.js';
|
|
10
10
|
import { FILES, PATHS, DEFAULT_SCRIPT_FOLDER } from './paths.js';
|
|
11
|
-
|
|
11
|
+
import { UserCancelledError } from './errors.js';
|
|
12
|
+
const VERSION = '0.5.0';
|
|
12
13
|
/**
|
|
13
14
|
* Apply CLI flags on top of the loaded config. Flags only override; they
|
|
14
15
|
* never widen or compose with each other implicitly.
|
|
@@ -154,7 +155,13 @@ export async function main(argv) {
|
|
|
154
155
|
// Footer counts only commands that actually executed. Declined or
|
|
155
156
|
// cancelled commands appear in `result.commands` for the history
|
|
156
157
|
// log but don't count as "ran" since no subprocess was started.
|
|
157
|
-
|
|
158
|
+
//
|
|
159
|
+
// Gated on `cfg.verbose`: the footer is supplementary information
|
|
160
|
+
// ("here's what happened during the run") that's useful while you're
|
|
161
|
+
// watching the agent work, but noisy for scripted/pipeline use. With
|
|
162
|
+
// verbose off, nothing aca generates reaches the terminal — only the
|
|
163
|
+
// AWS CLI's verbatim output does.
|
|
164
|
+
if (cfg.verbose && result.executedCommandCount > 0) {
|
|
158
165
|
const tag = result.profile ? `[${result.profile}]` : '';
|
|
159
166
|
const cmds = result.executedCommandCount === 1
|
|
160
167
|
? '1 command'
|
|
@@ -163,10 +170,19 @@ export async function main(argv) {
|
|
|
163
170
|
}
|
|
164
171
|
}
|
|
165
172
|
catch (err) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
// User cancelled (Ctrl-C at a prompt). Print a calm message,
|
|
174
|
+
// exit 130 (SIGINT convention), no red error, no "ran N" footer,
|
|
175
|
+
// no stack trace.
|
|
176
|
+
if (err instanceof UserCancelledError) {
|
|
177
|
+
process.stderr.write(chalk.dim('cancelled by user\n'));
|
|
178
|
+
process.exitCode = 130;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
182
|
+
logger.error('Agent failed', msg);
|
|
183
|
+
process.stderr.write(chalk.red('Error: ') + msg + '\n');
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
}
|
|
170
186
|
}
|
|
171
187
|
finally {
|
|
172
188
|
logger.close();
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel error: the user pressed Ctrl-C during a prompt. Thrown from
|
|
3
|
+
* inside tool `execute()` functions when Inquirer throws ExitPromptError,
|
|
4
|
+
* propagated up through the agent loop, caught at the cli.ts boundary
|
|
5
|
+
* where it triggers a clean exit with status 130.
|
|
6
|
+
*
|
|
7
|
+
* Using a custom class (not a string match) gives us reliable
|
|
8
|
+
* `instanceof UserCancelledError` checks across all the places that need
|
|
9
|
+
* to handle the cancellation differently from real errors.
|
|
10
|
+
*/
|
|
11
|
+
export declare class UserCancelledError extends Error {
|
|
12
|
+
constructor(message?: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Sentinel error: the AWS CLI returned an exit code in FATAL_AWS_EXIT_CODES
|
|
16
|
+
* (252-255). These indicate an unrecoverable condition — auth failure,
|
|
17
|
+
* missing credentials, malformed request, AWS service failure — and
|
|
18
|
+
* retrying won't help. The tool throws this instead of returning a result,
|
|
19
|
+
* so the model never gets a chance to retry. The agent loop catches it,
|
|
20
|
+
* propagates the stderr to the user, and exits 1.
|
|
21
|
+
*
|
|
22
|
+
* Carries the original cmd, exitCode, and stderr so cli.ts can surface
|
|
23
|
+
* them to the user.
|
|
24
|
+
*/
|
|
25
|
+
export declare class FatalAwsCliError extends Error {
|
|
26
|
+
readonly cmd: string;
|
|
27
|
+
readonly exitCode: number;
|
|
28
|
+
readonly stderr: string;
|
|
29
|
+
constructor(cmd: string, exitCode: number, stderr: string);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* AWS CLI exit codes that indicate an unrecoverable condition:
|
|
33
|
+
* 252 — Command-line parsing errors (typically a bug in our agent or
|
|
34
|
+
* the CLI itself; retrying won't help)
|
|
35
|
+
* 253 — Profile/credentials not found in the credential chain
|
|
36
|
+
* 254 — Client-side error (4xx from the service — auth, permission,
|
|
37
|
+
* malformed request)
|
|
38
|
+
* 255 — Server-side error (5xx from the service — internal AWS issues)
|
|
39
|
+
*
|
|
40
|
+
* Anything else non-zero is a soft error (resource not found, etc.) and
|
|
41
|
+
* gets returned to the model normally — it may try a different approach.
|
|
42
|
+
* The model is bounded by `maxSteps` for runaway loops; we deliberately
|
|
43
|
+
* don't impose a separate soft-failure cap.
|
|
44
|
+
*
|
|
45
|
+
* Exit code 130 (SIGINT) in interactive mode is treated as a clean user
|
|
46
|
+
* cancellation, not an error — see aws-cli.ts's `effectivelyOk` rule.
|
|
47
|
+
*/
|
|
48
|
+
export declare const FATAL_AWS_EXIT_CODES: Set<number>;
|
|
49
|
+
/**
|
|
50
|
+
* Wrap an Inquirer prompt promise so that Ctrl-C (which Inquirer reports
|
|
51
|
+
* as `ExitPromptError`) becomes our `UserCancelledError` sentinel. The
|
|
52
|
+
* Inquirer error class isn't easily importable, so we detect by `.name`.
|
|
53
|
+
* Re-throws any other error unchanged.
|
|
54
|
+
*
|
|
55
|
+
* const answer = await wrapPrompt(confirm({ message: '...' }));
|
|
56
|
+
*/
|
|
57
|
+
export declare function wrapPrompt<T>(p: Promise<T>): Promise<T>;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel error: the user pressed Ctrl-C during a prompt. Thrown from
|
|
3
|
+
* inside tool `execute()` functions when Inquirer throws ExitPromptError,
|
|
4
|
+
* propagated up through the agent loop, caught at the cli.ts boundary
|
|
5
|
+
* where it triggers a clean exit with status 130.
|
|
6
|
+
*
|
|
7
|
+
* Using a custom class (not a string match) gives us reliable
|
|
8
|
+
* `instanceof UserCancelledError` checks across all the places that need
|
|
9
|
+
* to handle the cancellation differently from real errors.
|
|
10
|
+
*/
|
|
11
|
+
export class UserCancelledError extends Error {
|
|
12
|
+
constructor(message = 'User cancelled the operation.') {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'UserCancelledError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sentinel error: the AWS CLI returned an exit code in FATAL_AWS_EXIT_CODES
|
|
19
|
+
* (252-255). These indicate an unrecoverable condition — auth failure,
|
|
20
|
+
* missing credentials, malformed request, AWS service failure — and
|
|
21
|
+
* retrying won't help. The tool throws this instead of returning a result,
|
|
22
|
+
* so the model never gets a chance to retry. The agent loop catches it,
|
|
23
|
+
* propagates the stderr to the user, and exits 1.
|
|
24
|
+
*
|
|
25
|
+
* Carries the original cmd, exitCode, and stderr so cli.ts can surface
|
|
26
|
+
* them to the user.
|
|
27
|
+
*/
|
|
28
|
+
export class FatalAwsCliError extends Error {
|
|
29
|
+
cmd;
|
|
30
|
+
exitCode;
|
|
31
|
+
stderr;
|
|
32
|
+
constructor(cmd, exitCode, stderr) {
|
|
33
|
+
super(`AWS CLI exited with code ${exitCode} (unrecoverable): ${stderr.trim() || '<no stderr>'}`);
|
|
34
|
+
this.cmd = cmd;
|
|
35
|
+
this.exitCode = exitCode;
|
|
36
|
+
this.stderr = stderr;
|
|
37
|
+
this.name = 'FatalAwsCliError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* AWS CLI exit codes that indicate an unrecoverable condition:
|
|
42
|
+
* 252 — Command-line parsing errors (typically a bug in our agent or
|
|
43
|
+
* the CLI itself; retrying won't help)
|
|
44
|
+
* 253 — Profile/credentials not found in the credential chain
|
|
45
|
+
* 254 — Client-side error (4xx from the service — auth, permission,
|
|
46
|
+
* malformed request)
|
|
47
|
+
* 255 — Server-side error (5xx from the service — internal AWS issues)
|
|
48
|
+
*
|
|
49
|
+
* Anything else non-zero is a soft error (resource not found, etc.) and
|
|
50
|
+
* gets returned to the model normally — it may try a different approach.
|
|
51
|
+
* The model is bounded by `maxSteps` for runaway loops; we deliberately
|
|
52
|
+
* don't impose a separate soft-failure cap.
|
|
53
|
+
*
|
|
54
|
+
* Exit code 130 (SIGINT) in interactive mode is treated as a clean user
|
|
55
|
+
* cancellation, not an error — see aws-cli.ts's `effectivelyOk` rule.
|
|
56
|
+
*/
|
|
57
|
+
export const FATAL_AWS_EXIT_CODES = new Set([252, 253, 254, 255]);
|
|
58
|
+
/**
|
|
59
|
+
* Wrap an Inquirer prompt promise so that Ctrl-C (which Inquirer reports
|
|
60
|
+
* as `ExitPromptError`) becomes our `UserCancelledError` sentinel. The
|
|
61
|
+
* Inquirer error class isn't easily importable, so we detect by `.name`.
|
|
62
|
+
* Re-throws any other error unchanged.
|
|
63
|
+
*
|
|
64
|
+
* const answer = await wrapPrompt(confirm({ message: '...' }));
|
|
65
|
+
*/
|
|
66
|
+
export async function wrapPrompt(p) {
|
|
67
|
+
try {
|
|
68
|
+
return await p;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (err instanceof Error && err.name === 'ExitPromptError') {
|
|
72
|
+
throw new UserCancelledError();
|
|
73
|
+
}
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
}
|
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-/,
|
|
@@ -164,7 +165,7 @@ export function awsCliTool(opts) {
|
|
|
164
165
|
if (useInteractive) {
|
|
165
166
|
process.stderr.write(`${chalk.bold(' Mode: ')}${chalk.yellow('interactive')} (your terminal will be connected to the command)\n`);
|
|
166
167
|
}
|
|
167
|
-
const ok = await confirm({ message: 'Execute this command?', default: true });
|
|
168
|
+
const ok = await wrapPrompt(confirm({ message: 'Execute this command?', default: true }));
|
|
168
169
|
if (!ok) {
|
|
169
170
|
opts.logger.warn('User declined command');
|
|
170
171
|
// Record the declined call so the agent's end-of-run logic sees
|
|
@@ -208,9 +209,18 @@ export function awsCliTool(opts) {
|
|
|
208
209
|
opts.logger.trace('stderr', stderr);
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
|
-
else if (code !== 0) {
|
|
212
|
+
else if (code !== 0 && code !== 130) {
|
|
213
|
+
// Exit 130 in interactive mode = user pressed Ctrl-C to end their
|
|
214
|
+
// SSM session, shell, port-forward, etc. That's expected, not a
|
|
215
|
+
// failure. Anything else is genuine — log it.
|
|
212
216
|
opts.logger.warn(`Interactive AWS CLI exited non-zero (${code})`);
|
|
213
217
|
}
|
|
218
|
+
// SSM sessions and other interactive AWS CLI commands return 130
|
|
219
|
+
// when the user Ctrl-Cs to end the session. That's the normal way
|
|
220
|
+
// to leave a shell — treat it as success for ok/exit purposes so we
|
|
221
|
+
// don't surface it as an error in cli.ts. The real exit code is
|
|
222
|
+
// still recorded in the audit log for accuracy.
|
|
223
|
+
const effectivelyOk = code === 0 || (useInteractive && code === 130);
|
|
214
224
|
// Audit captures whatever we have. For interactive runs stdout/stderr
|
|
215
225
|
// are empty — that's accurate, the bytes went to the terminal — and
|
|
216
226
|
// the audit entry serves as a record that "an interactive session
|
|
@@ -219,7 +229,7 @@ export function awsCliTool(opts) {
|
|
|
219
229
|
cmd: display,
|
|
220
230
|
profile,
|
|
221
231
|
exitCode: code,
|
|
222
|
-
ok:
|
|
232
|
+
ok: effectivelyOk,
|
|
223
233
|
stdout: useInteractive ? '[interactive session — output not captured]' : stdout,
|
|
224
234
|
stderr: useInteractive ? '' : stderr,
|
|
225
235
|
});
|
|
@@ -234,18 +244,33 @@ export function awsCliTool(opts) {
|
|
|
234
244
|
: stdout,
|
|
235
245
|
stderr: useInteractive ? '' : stderr,
|
|
236
246
|
exitCode: code,
|
|
237
|
-
ok:
|
|
247
|
+
ok: effectivelyOk,
|
|
238
248
|
});
|
|
239
249
|
// For the agent's context, return a clear signal that interactive
|
|
240
250
|
// mode ran so it doesn't try to parse fictional stdout.
|
|
241
251
|
if (useInteractive) {
|
|
242
252
|
return {
|
|
243
|
-
ok:
|
|
253
|
+
ok: effectivelyOk,
|
|
244
254
|
exitCode: code,
|
|
245
255
|
interactive: true,
|
|
246
256
|
note: 'Interactive session ran. Output went directly to the user\'s terminal and was not captured. Do not summarize or describe its contents.',
|
|
247
257
|
};
|
|
248
258
|
}
|
|
259
|
+
// Classify failures. Fatal exit codes (252-255) indicate the call
|
|
260
|
+
// won't succeed without external intervention — bad credentials,
|
|
261
|
+
// missing resource, malformed request, AWS service failure. We
|
|
262
|
+
// throw FatalAwsCliError (after recording the audit trail above)
|
|
263
|
+
// rather than returning a result to the model: the throw unwinds
|
|
264
|
+
// the agent loop entirely, the user sees the AWS stderr in red,
|
|
265
|
+
// and we exit 1. The model never gets a chance to retry, because
|
|
266
|
+
// these classes of error don't get better with retries.
|
|
267
|
+
//
|
|
268
|
+
// Soft failures (other non-zero exits) ARE returned to the model
|
|
269
|
+
// as ordinary results. The model may retry with a different
|
|
270
|
+
// approach. maxSteps bounds the worst case if that goes nowhere.
|
|
271
|
+
if (code !== 0 && FATAL_AWS_EXIT_CODES.has(code)) {
|
|
272
|
+
throw new FatalAwsCliError(display, code, stderr);
|
|
273
|
+
}
|
|
249
274
|
return {
|
|
250
275
|
ok: code === 0,
|
|
251
276
|
exitCode: code,
|
|
@@ -254,6 +279,11 @@ export function awsCliTool(opts) {
|
|
|
254
279
|
};
|
|
255
280
|
}
|
|
256
281
|
catch (err) {
|
|
282
|
+
// FatalAwsCliError is our own signal — propagate it cleanly.
|
|
283
|
+
// UserCancelledError must propagate too (Ctrl-C at the approval
|
|
284
|
+
// prompt) or it'd get swallowed into a spawn-failure log entry.
|
|
285
|
+
if (err instanceof FatalAwsCliError || err instanceof UserCancelledError)
|
|
286
|
+
throw err;
|
|
257
287
|
const msg = err instanceof Error ? err.message : String(err);
|
|
258
288
|
opts.logger.error('Failed to spawn aws CLI', msg);
|
|
259
289
|
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(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
|
+
}));
|
|
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,28 @@ 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
|
-
const answer = await select({
|
|
48
|
+
const answer = await wrapPrompt(select({
|
|
48
49
|
message: q.message,
|
|
49
50
|
choices: q.choices.map((c) => ({ value: c, name: c })),
|
|
50
51
|
default: q.defaultValue,
|
|
51
|
-
});
|
|
52
|
+
}));
|
|
52
53
|
return answer;
|
|
53
54
|
}
|
|
54
55
|
case 'confirm': {
|
|
55
56
|
const def = (q.defaultValue ?? 'yes').toLowerCase().startsWith('y');
|
|
56
|
-
const answer = await confirm({ message: q.message, default: def });
|
|
57
|
+
const answer = await wrapPrompt(confirm({ message: q.message, default: def }));
|
|
57
58
|
return answer ? 'yes' : 'no';
|
|
58
59
|
}
|
|
59
60
|
case 'secret': {
|
|
60
61
|
// Inquirer's password prompt masks input. Used for short secrets like
|
|
61
62
|
// MFA codes; long-lived AWS credentials should always come from the
|
|
62
63
|
// user's profile, not be typed here.
|
|
63
|
-
const answer = await password({ message: q.message, mask: '*' });
|
|
64
|
+
const answer = await wrapPrompt(password({ message: q.message, mask: '*' }));
|
|
64
65
|
return answer;
|
|
65
66
|
}
|
|
66
67
|
case 'text':
|
|
67
68
|
default: {
|
|
68
|
-
const answer = await input({ message: q.message, default: q.defaultValue });
|
|
69
|
+
const answer = await wrapPrompt(input({ message: q.message, default: q.defaultValue }));
|
|
69
70
|
return answer;
|
|
70
71
|
}
|
|
71
72
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aws-cli-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@ai-sdk/google": "^3.0.74",
|
|
57
57
|
"@ai-sdk/openai": "^3.0.64",
|
|
58
58
|
"@aws-sdk/credential-providers": "^3.1046.0",
|
|
59
|
-
"@inquirer/prompts": "^
|
|
59
|
+
"@inquirer/prompts": "^8.4.3",
|
|
60
60
|
"ai": "^6.0.183",
|
|
61
61
|
"chalk": "^5.4.0",
|
|
62
62
|
"commander": "^13.0.0",
|