aws-cli-agent 0.5.0 → 0.6.1
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 +63 -3
- package/README.md +53 -12
- package/dist/agent.js +5 -3
- package/dist/cli.js +15 -2
- package/dist/config.d.ts +45 -4
- package/dist/config.js +157 -47
- package/dist/errors.d.ts +28 -6
- package/dist/errors.js +27 -7
- package/dist/providers.d.ts +22 -9
- package/dist/providers.js +82 -27
- package/dist/tools/aws-cli.d.ts +15 -0
- package/dist/tools/aws-cli.js +47 -8
- package/dist/tools/bash.js +111 -3
- package/dist/tools/prompt.js +9 -6
- package/package.json +14 -14
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,69 @@ 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.6.1] - 2026-05-31
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **AWS commands are highlighted in the bash script approval preview.** When the agent generates a script for execution, AWS CLI invocations now stand out from the rest of the script body with inverse-color "highlighter strips" so the mutating calls are easy to spot in a long script:
|
|
12
|
+
- Read-only AWS calls (`describe-*`, `list-*`, `get-*`, `s3 ls`, `sts get-caller-identity`, etc.) render as a **blue strip**.
|
|
13
|
+
- Mutating AWS calls (`delete-*`, `terminate-*`, `create-*`, `put-*`, `s3 rm`, `s3 cp`, etc.) render as a **yellow strip**.
|
|
14
|
+
- Everything else — shell control flow, comments, args, flags, pipelines — stays in the existing green script body color.
|
|
15
|
+
|
|
16
|
+
Detection is pattern-based, not a full shell parser: `aws <service> <verb>` is matched wherever it appears in a line (start of line, after a pipe, after `time` or `env VAR=val`, inside a `$(...)` substitution). Read-only vs. mutating classification uses the same `READ_ONLY_VERBS` / `READ_ONLY_FULL` lists that drive the per-command auto-approve decision — single source of truth, no drift between the two features. Adding a verb to either list affects both behaviors.
|
|
17
|
+
|
|
18
|
+
False positives (e.g. an `aws ec2 describe-instances` substring inside an `echo "..."` literal) get highlighted too. Accepted limitation: a real shell tokenizer would catch these, but the surrounding `echo` and quotes make them visually distinguishable in context.
|
|
19
|
+
|
|
20
|
+
## [0.6.0] - 2026-05-31
|
|
21
|
+
|
|
22
|
+
### Breaking
|
|
23
|
+
|
|
24
|
+
- **Config schema restructured: per-provider blocks at top level.** Provider-specific fields (`model`, `apiKey`, `apiKeyEnv`) move into a top-level block named after the provider. The old top-level `model` and `apiKeyEnv` fields are gone. For Bedrock, the `model` field also moves into the existing `bedrock` block (which already held `region` and `profile`).
|
|
25
|
+
|
|
26
|
+
Old:
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"provider": "anthropic",
|
|
30
|
+
"model": "claude-sonnet-4-6",
|
|
31
|
+
"apiKeyEnv": "MY_KEY",
|
|
32
|
+
"bedrock": { "region": "us-east-1" }
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
New:
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"provider": "anthropic",
|
|
40
|
+
"anthropic": {
|
|
41
|
+
"model": "claude-sonnet-4-6",
|
|
42
|
+
"apiKeyEnv": "MY_KEY"
|
|
43
|
+
},
|
|
44
|
+
"bedrock": {
|
|
45
|
+
"model": "us.anthropic.claude-sonnet-4-6",
|
|
46
|
+
"region": "us-east-1"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Old configs fail to load with a clear migration message pointing at the new shape — no silent fallback.
|
|
52
|
+
|
|
53
|
+
- **Strict validation of the active provider block.** When `provider` is set, the matching top-level block must exist with a `model` field. Previously the top-level `model` default kicked in if absent; now you have to be explicit. Run `aca config` to scaffold a working default.
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- **`<provider>.apiKey` config field for Anthropic / OpenAI / Google.** Convenience for casual users who don't want to set env vars; persists to disk so see the security note in the README. Resolution order: `apiKeyEnv`-named env var → default provider env var → `<provider>.apiKey` from config → error. The env var always wins when both are set.
|
|
58
|
+
- **Default config file is created with mode `0600`** (owner read/write only) so it isn't world-readable on creation. Doesn't help if you edit the file with another tool that resets permissions.
|
|
59
|
+
- **Debug-level log note when the API key resolves from config** rather than an env var. Helps post-hoc forensics see what happened. The key value itself is never logged.
|
|
60
|
+
- **Helpful migration error for pre-0.6 configs.** Loading a config file with top-level `model` or `apiKeyEnv` produces a side-by-side old/new shape diff instead of a cryptic zod parse failure.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- **`apiKeyEnv` moves from top-level into the per-provider block.** It was a top-level field since 0.1.0; now it lives at `<provider>.apiKeyEnv`. Same semantics, just nested.
|
|
65
|
+
- **`apiKeyEnv` set to an empty env var emits a warning.** Previously the fall-through to the default env var was silent — convenient until a user noticed the wrong account was being charged. Now if `<provider>.apiKeyEnv` names a variable that isn't set, `aca` prints a warning to stderr and falls back to the default env var (and then to `<provider>.apiKey`). The warning is purely informational; resolution still continues.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
- **Ctrl-C inside an SSM session no longer prints "Cannot perform start session: read /dev/stdin: input/output error".** Previously, Ctrl-C delivered SIGINT to both the AWS CLI subprocess AND aca; aca's process tore down the shared stdin before the AWS CLI's own cleanup completed, producing the I/O-error message. The fix: `aca` installs a no-op SIGINT handler for the lifetime of any interactive AWS CLI subprocess, leaving the signal exclusively to the child. The AWS CLI now performs its normal clean shutdown and exits with code 0 or 130, which aca recognizes as a clean termination.
|
|
8
70
|
|
|
9
71
|
### Added
|
|
10
72
|
|
|
@@ -35,8 +97,6 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
35
97
|
With verbose off, nothing aca generates reaches the terminal — only
|
|
36
98
|
the AWS CLI's verbatim output does, matching the README's promise.
|
|
37
99
|
|
|
38
|
-
## [0.4.0] - 2026-05-18
|
|
39
|
-
|
|
40
100
|
### Changed
|
|
41
101
|
|
|
42
102
|
- **Dependency upgrades.** Vercel AI SDK v4 → v6, zod v3 → v4, TypeScript
|
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ The first example is interactive — the agent runs a read-only `describe-instan
|
|
|
31
31
|
- **Your prompts go to the model provider.** AWS CLI output is fed back to the model as part of subsequent steps. That means resource names, instance IDs, tag values, and any other data that appears in command output is transmitted to Anthropic / OpenAI / Google / Bedrock (depending on your provider choice). The provider does not retain this data beyond the request itself (and the cache TTL, ~5 minutes for cached prefixes), but **confirm this is compatible with the policies you have to respect** before pointing `aca` at sensitive accounts.
|
|
32
32
|
- **Provider terms apply.** When you use a provider, you agree to that provider's terms of service. For Bedrock, that's AWS's own terms (data stays in your AWS account boundary). For Anthropic / OpenAI / Google, that's their respective enterprise / API terms. Read them.
|
|
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
|
+
- **API keys in the config file persist to disk.** As of 0.6.0, you can put your LLM provider API key in `<provider>.apiKey` for convenience. The env var still takes precedence when both are set. Putting a key on disk is a meaningful step down from keeping it in your shell environment: backup tools, accidental `git add .` from the wrong directory, screen-sharing, and shoulder-surfing all become realistic leak vectors. The config file is created with mode 0600 by `aca config`, but that doesn't help if you edit it with another tool that resets permissions, or if you copy your `~/.config` into a dotfiles repo. Use the env var path when possible; reserve the config-file path for casual local use.
|
|
34
35
|
- **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
36
|
|
|
36
37
|
## Trademark & affiliation
|
|
@@ -78,7 +79,9 @@ Default contents (created by `aca config`):
|
|
|
78
79
|
```json
|
|
79
80
|
{
|
|
80
81
|
"provider": "anthropic",
|
|
81
|
-
"
|
|
82
|
+
"anthropic": {
|
|
83
|
+
"model": "claude-sonnet-4-6"
|
|
84
|
+
},
|
|
82
85
|
"maxSteps": 15,
|
|
83
86
|
"logging": {
|
|
84
87
|
"level": "error",
|
|
@@ -97,28 +100,43 @@ Default contents (created by `aca config`):
|
|
|
97
100
|
}
|
|
98
101
|
```
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
A more populated config showing all four providers (you only need the block for the active provider; the others are illustrative):
|
|
101
104
|
|
|
102
105
|
```json
|
|
103
106
|
{
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
|
|
107
|
+
"provider": "anthropic",
|
|
108
|
+
"anthropic": {
|
|
109
|
+
"model": "claude-sonnet-4-6",
|
|
110
|
+
"apiKey": "sk-ant-...",
|
|
111
|
+
"apiKeyEnv": "MY_CUSTOM_ANTHROPIC_KEY"
|
|
112
|
+
},
|
|
113
|
+
"openai": {
|
|
114
|
+
"model": "gpt-5",
|
|
115
|
+
"apiKey": "sk-..."
|
|
116
|
+
},
|
|
117
|
+
"google": {
|
|
118
|
+
"model": "gemini-2.5-pro",
|
|
119
|
+
"apiKey": "..."
|
|
120
|
+
},
|
|
107
121
|
"bedrock": {
|
|
122
|
+
"model": "us.anthropic.claude-sonnet-4-6",
|
|
108
123
|
"region": "us-east-1",
|
|
109
124
|
"profile": "shared-services"
|
|
110
|
-
}
|
|
125
|
+
},
|
|
126
|
+
"defaultRegion": "eu-west-1",
|
|
127
|
+
"scriptFolder": "/home/me/aws-scripts"
|
|
111
128
|
}
|
|
112
129
|
```
|
|
113
130
|
|
|
131
|
+
Per-provider configuration lives in a top-level block named after the provider (`anthropic`, `openai`, `google`, `bedrock`). The active provider is selected by the top-level `provider` field, and its block must exist with at least a `model` set. All other provider blocks are ignored at runtime, but you can keep them populated if you switch providers often — `aca` won't read them until you change `provider`.
|
|
132
|
+
|
|
114
133
|
### Top-level keys
|
|
115
134
|
|
|
116
135
|
| Key | Default | Meaning |
|
|
117
136
|
|---|---|---|
|
|
118
|
-
| `provider` | `anthropic` | LLM provider: `anthropic` \| `openai` \| `google` \| `bedrock` |
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `bedrock` | — | Bedrock-specific settings (see below). Only used when `provider = "bedrock"`. |
|
|
137
|
+
| `provider` | `anthropic` | Active LLM provider: `anthropic` \| `openai` \| `google` \| `bedrock`. The matching top-level block must exist and contain a `model`. |
|
|
138
|
+
| `anthropic`, `openai`, `google` | — | Per-provider blocks for the three keyed providers. See "Per-provider keys" below. |
|
|
139
|
+
| `bedrock` | — | Bedrock provider block. See "Bedrock" below. |
|
|
122
140
|
| `defaultRegion` | — | AWS region injected into every AWS CLI command when the agent didn't specify one |
|
|
123
141
|
| `caching` | `true` | Enable prompt caching for providers that support it. See "Prompt caching" below. |
|
|
124
142
|
| `maxSteps` | `15` | Hard cap on agent reasoning/tool steps per request (range 1-50) |
|
|
@@ -130,6 +148,28 @@ Optional fields (omit if you don't need them):
|
|
|
130
148
|
| `historyLimit` | `200` | Max history entries kept in memory for context |
|
|
131
149
|
| `scriptFolder` | `$XDG_DATA_HOME/aws-cli-agent/scripts` | Where saved bash scripts are written |
|
|
132
150
|
|
|
151
|
+
### Per-provider keys (anthropic, openai, google)
|
|
152
|
+
|
|
153
|
+
All three keyed providers accept the same three fields:
|
|
154
|
+
|
|
155
|
+
| Key | Required | Meaning |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `<provider>.model` | yes | Model identifier (e.g. `claude-sonnet-4-6`, `gpt-5`, `gemini-2.5-pro`). Required when this provider is active. |
|
|
158
|
+
| `<provider>.apiKey` | no | API key for this provider. **SECURITY:** putting the key here persists it to disk; prefer the env var. The env var wins if both are set. |
|
|
159
|
+
| `<provider>.apiKeyEnv` | no | Override the env var name read for this provider. Default names: `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY`. |
|
|
160
|
+
|
|
161
|
+
**API key resolution order** (highest priority first):
|
|
162
|
+
|
|
163
|
+
1. Env var named by `<provider>.apiKeyEnv` (if that field is set).
|
|
164
|
+
2. Default env var for the provider (e.g. `ANTHROPIC_API_KEY`).
|
|
165
|
+
3. `<provider>.apiKey` from the config file.
|
|
166
|
+
|
|
167
|
+
If none of the above is set, the run fails with an error listing all three options.
|
|
168
|
+
|
|
169
|
+
When the key is resolved from the config file instead of an env var, `aca` writes a debug-level note to `general.log` ("API key loaded from config file (no env var set)") so a forensic investigation can see what happened. The key value itself is never logged.
|
|
170
|
+
|
|
171
|
+
The config file is created with file mode `0600` (owner read/write only) when `aca config` runs. If you edit it manually with another tool, `aca` won't re-permission it on read — that's on you.
|
|
172
|
+
|
|
133
173
|
### Logging
|
|
134
174
|
|
|
135
175
|
```json
|
|
@@ -160,13 +200,13 @@ aca --region eu-west-1 "list ec2 instances in my-staging"
|
|
|
160
200
|
|
|
161
201
|
### Bedrock
|
|
162
202
|
|
|
163
|
-
When `provider = "bedrock"`, configure region and (optionally) profile
|
|
203
|
+
When `provider = "bedrock"`, configure model, region, and (optionally) profile in the `bedrock` block:
|
|
164
204
|
|
|
165
205
|
```json
|
|
166
206
|
{
|
|
167
207
|
"provider": "bedrock",
|
|
168
|
-
"model": "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
|
|
169
208
|
"bedrock": {
|
|
209
|
+
"model": "us.anthropic.claude-sonnet-4-6",
|
|
170
210
|
"region": "us-east-1",
|
|
171
211
|
"profile": "shared-services"
|
|
172
212
|
}
|
|
@@ -176,6 +216,7 @@ When `provider = "bedrock"`, configure region and (optionally) profile via the n
|
|
|
176
216
|
- **Model IDs**: Bedrock uses fully-qualified inference-profile IDs. The `us.` / `eu.` / `apac.` prefix is required for most newer Anthropic models. Use `aws bedrock list-inference-profiles --region <region>` to discover what your account can invoke.
|
|
177
217
|
- **`bedrock.profile`** is independent from operational profiles. The agent calls Bedrock under `bedrock.profile`, but each AWS CLI command it issues uses its own `--profile` (resolved from the user's prompt / history). This is the right pattern when one account holds Bedrock entitlements and other accounts hold workloads.
|
|
178
218
|
- If `bedrock.region` is unset, falls back to `AWS_REGION` / `AWS_DEFAULT_REGION` env vars.
|
|
219
|
+
- Bedrock uses the AWS credential chain — there's no `apiKey` field.
|
|
179
220
|
|
|
180
221
|
### Prompt caching
|
|
181
222
|
|
package/dist/agent.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { streamText, stepCountIs } from 'ai';
|
|
2
|
+
import { getActiveModel } from './config.js';
|
|
2
3
|
import { createModel } from './providers.js';
|
|
3
4
|
import { createTools } from './tools/index.js';
|
|
4
5
|
import { FatalAwsCliError, UserCancelledError } from './errors.js';
|
|
@@ -60,8 +61,9 @@ export async function runAgent(opts) {
|
|
|
60
61
|
// the tools array is sent at full cost on every request.
|
|
61
62
|
const useCaching = config.caching && (config.provider === 'anthropic' || config.provider === 'bedrock');
|
|
62
63
|
const tools = createTools({ logger, config, history, audit, record });
|
|
63
|
-
const model = createModel(config);
|
|
64
|
-
|
|
64
|
+
const model = createModel(config, logger);
|
|
65
|
+
const activeModel = getActiveModel(config);
|
|
66
|
+
logger.info(`Starting agent (provider=${config.provider}, model=${activeModel})`);
|
|
65
67
|
logger.debug('User input', input);
|
|
66
68
|
reasoning.beginRun(input);
|
|
67
69
|
// Inline a small recent-history hint so the model has soft context even
|
|
@@ -328,7 +330,7 @@ export async function runAgent(opts) {
|
|
|
328
330
|
usage.log({
|
|
329
331
|
input,
|
|
330
332
|
provider: config.provider,
|
|
331
|
-
model:
|
|
333
|
+
model: activeModel,
|
|
332
334
|
steps: finalSteps.length,
|
|
333
335
|
promptTokens: totalUsage?.inputTokens ?? 0,
|
|
334
336
|
completionTokens: totalUsage?.outputTokens ?? 0,
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { loadConfig, writeDefaultConfig } from './config.js';
|
|
3
|
+
import { loadConfig, validateActiveProvider, writeDefaultConfig } from './config.js';
|
|
4
4
|
import { Logger } from './logger.js';
|
|
5
5
|
import { AuditLogger } from './audit.js';
|
|
6
6
|
import { ReasoningLogger } from './reasoning.js';
|
|
@@ -9,7 +9,7 @@ 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.
|
|
12
|
+
const VERSION = '0.6.1';
|
|
13
13
|
/**
|
|
14
14
|
* Apply CLI flags on top of the loaded config. Flags only override; they
|
|
15
15
|
* never widen or compose with each other implicitly.
|
|
@@ -103,6 +103,19 @@ export async function main(argv) {
|
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
105
|
const cfg = applyCliOverrides(loadConfig(), globalOpts);
|
|
106
|
+
// Strict validation: the active provider must have a config block with
|
|
107
|
+
// a `model` set. Deferred from loadConfig so subcommands like `paths`
|
|
108
|
+
// and `history` work even without a config file. The run command is
|
|
109
|
+
// the one that actually needs a complete provider, so we check here.
|
|
110
|
+
try {
|
|
111
|
+
validateActiveProvider(cfg);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
115
|
+
process.stderr.write(chalk.red('Config error: ') + msg + '\n');
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
106
119
|
const logger = new Logger(cfg.logging.level);
|
|
107
120
|
const audit = new AuditLogger(cfg.logging.auditLog);
|
|
108
121
|
const reasoning = new ReasoningLogger({
|
package/dist/config.d.ts
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare const ConfigSchema: z.ZodObject<{
|
|
3
3
|
provider: z.ZodDefault<z.ZodEnum<{
|
|
4
|
-
bedrock: "bedrock";
|
|
5
4
|
anthropic: "anthropic";
|
|
6
5
|
openai: "openai";
|
|
7
6
|
google: "google";
|
|
7
|
+
bedrock: "bedrock";
|
|
8
8
|
}>>;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
anthropic: z.ZodOptional<z.ZodObject<{
|
|
10
|
+
model: z.ZodOptional<z.ZodString>;
|
|
11
|
+
apiKey: z.ZodOptional<z.ZodString>;
|
|
12
|
+
apiKeyEnv: z.ZodOptional<z.ZodString>;
|
|
13
|
+
}, z.core.$strip>>;
|
|
14
|
+
openai: z.ZodOptional<z.ZodObject<{
|
|
15
|
+
model: z.ZodOptional<z.ZodString>;
|
|
16
|
+
apiKey: z.ZodOptional<z.ZodString>;
|
|
17
|
+
apiKeyEnv: z.ZodOptional<z.ZodString>;
|
|
18
|
+
}, z.core.$strip>>;
|
|
19
|
+
google: z.ZodOptional<z.ZodObject<{
|
|
20
|
+
model: z.ZodOptional<z.ZodString>;
|
|
21
|
+
apiKey: z.ZodOptional<z.ZodString>;
|
|
22
|
+
apiKeyEnv: z.ZodOptional<z.ZodString>;
|
|
23
|
+
}, z.core.$strip>>;
|
|
11
24
|
bedrock: z.ZodOptional<z.ZodObject<{
|
|
25
|
+
model: z.ZodOptional<z.ZodString>;
|
|
12
26
|
region: z.ZodOptional<z.ZodString>;
|
|
13
27
|
profile: z.ZodOptional<z.ZodString>;
|
|
14
28
|
}, z.core.$strip>>;
|
|
@@ -38,6 +52,33 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
38
52
|
scriptFolder: z.ZodOptional<z.ZodString>;
|
|
39
53
|
}, z.core.$strip>;
|
|
40
54
|
export type Config = z.infer<typeof ConfigSchema>;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the active provider's model. The schema marks `model` optional
|
|
57
|
+
* per-block so that we can produce a single coherent error message in
|
|
58
|
+
* `validateActiveProvider` rather than zod's multi-issue tree. Call this
|
|
59
|
+
* only after validateActiveProvider has passed.
|
|
60
|
+
*/
|
|
61
|
+
export declare function getActiveModel(config: Config): string;
|
|
62
|
+
/**
|
|
63
|
+
* Strict post-parse validation for the active provider's block. The active
|
|
64
|
+
* provider's block must exist and must contain a `model`. Pre-1.0 we treat
|
|
65
|
+
* this as a hard error rather than scaffolding defaults, so the user always
|
|
66
|
+
* knows exactly what's being called and at what cost.
|
|
67
|
+
*
|
|
68
|
+
* Call this from code paths that actually run the agent — the `run` command.
|
|
69
|
+
* Subcommands that don't need a provider (`paths`, `config`, `history`)
|
|
70
|
+
* skip this check, so a user with no config file can still use them.
|
|
71
|
+
*/
|
|
72
|
+
export declare function validateActiveProvider(config: Config): void;
|
|
41
73
|
export declare function loadConfig(): Config;
|
|
42
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Write a default config file if none exists. Scaffolds only the active
|
|
76
|
+
* provider's block (just `model`), deliberately not creating slots for
|
|
77
|
+
* other providers (less to read) and not scaffolding `apiKey` (less
|
|
78
|
+
* temptation to put secrets on disk).
|
|
79
|
+
*
|
|
80
|
+
* Sets mode 0600 on the file. This doesn't protect against a user editing
|
|
81
|
+
* with `cp` or moving the file later, but ensures that the file as we
|
|
82
|
+
* create it isn't world-readable.
|
|
83
|
+
*/
|
|
43
84
|
export declare function writeDefaultConfig(): string;
|
package/dist/config.js
CHANGED
|
@@ -2,8 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { FILES, PATHS } from './paths.js';
|
|
4
4
|
/**
|
|
5
|
-
* Logging configuration. All
|
|
6
|
-
*
|
|
5
|
+
* Logging configuration. All keys are optional in the file; defaults tilt
|
|
6
|
+
* toward "quiet but auditable" — a tool that writes to your AWS account
|
|
7
7
|
* should leave a paper trail by default, but shouldn't be noisy on the
|
|
8
8
|
* console unless you ask.
|
|
9
9
|
*/
|
|
@@ -16,9 +16,6 @@ const LoggingSchema = z
|
|
|
16
16
|
reasoningLog: z.boolean().default(false),
|
|
17
17
|
usageLog: z.boolean().default(true),
|
|
18
18
|
})
|
|
19
|
-
// zod v4 requires .default() to receive a fully-typed value, not `{}`.
|
|
20
|
-
// We list every field explicitly here; values must match the inner
|
|
21
|
-
// .default()s above to avoid silently changing defaults.
|
|
22
19
|
.default({
|
|
23
20
|
level: 'error',
|
|
24
21
|
auditLog: true,
|
|
@@ -26,58 +23,72 @@ const LoggingSchema = z
|
|
|
26
23
|
usageLog: true,
|
|
27
24
|
});
|
|
28
25
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* Per-provider configuration for the three keyed providers (Anthropic,
|
|
27
|
+
* OpenAI, Google). Each block is optional in the file — but when the
|
|
28
|
+
* top-level `provider` is set to one of these, the matching block must
|
|
29
|
+
* exist AND contain a `model`. That validation runs in the post-parse
|
|
30
|
+
* step (see `validateActiveProvider`).
|
|
31
|
+
*
|
|
32
|
+
* `apiKey` SECURITY NOTE: Putting the key here means it persists to disk.
|
|
33
|
+
* Prefer the environment variable (default name, or override via
|
|
34
|
+
* `apiKeyEnv`). The env var always wins if both are set.
|
|
35
|
+
*/
|
|
36
|
+
const KeyedProviderSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
model: z.string().optional(),
|
|
39
|
+
apiKey: z.string().optional(),
|
|
40
|
+
apiKeyEnv: z.string().optional(),
|
|
41
|
+
})
|
|
42
|
+
.optional();
|
|
43
|
+
/**
|
|
44
|
+
* Bedrock has a different shape: no apiKey (uses the AWS credential chain),
|
|
45
|
+
* but adds region and profile. Like the keyed providers, this block is
|
|
46
|
+
* optional in the file but required when `provider = "bedrock"`.
|
|
31
47
|
*/
|
|
32
48
|
const BedrockSchema = z
|
|
33
49
|
.object({
|
|
50
|
+
model: z.string().optional(),
|
|
34
51
|
region: z.string().optional(),
|
|
35
52
|
profile: z.string().optional(),
|
|
36
53
|
})
|
|
37
54
|
.optional();
|
|
38
55
|
export const ConfigSchema = z.object({
|
|
39
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Which provider is active. The matching top-level block (anthropic /
|
|
58
|
+
* openai / google / bedrock) must exist and contain a `model`. Use
|
|
59
|
+
* `aca config` to see a working default scaffold.
|
|
60
|
+
*/
|
|
40
61
|
provider: z
|
|
41
62
|
.enum(['anthropic', 'openai', 'google', 'bedrock'])
|
|
42
63
|
.default('anthropic'),
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
*/
|
|
49
|
-
apiKeyEnv: z.string().optional(),
|
|
50
|
-
/**
|
|
51
|
-
* Bedrock provider settings (region + profile). Only used when
|
|
52
|
-
* provider = "bedrock". See providers.ts for fallback chain.
|
|
53
|
-
*/
|
|
64
|
+
// Per-provider blocks. Each is independently optional; the active
|
|
65
|
+
// provider's block is validated separately for completeness.
|
|
66
|
+
anthropic: KeyedProviderSchema,
|
|
67
|
+
openai: KeyedProviderSchema,
|
|
68
|
+
google: KeyedProviderSchema,
|
|
54
69
|
bedrock: BedrockSchema,
|
|
55
70
|
/**
|
|
56
|
-
* Default AWS region for AWS CLI commands the agent executes. Used when
|
|
57
|
-
* user didn't mention a region in the request and history didn't
|
|
58
|
-
* Overridable per-run with --region. Independent of
|
|
71
|
+
* Default AWS region for AWS CLI commands the agent executes. Used when
|
|
72
|
+
* the user didn't mention a region in the request and history didn't
|
|
73
|
+
* supply one. Overridable per-run with --region. Independent of
|
|
74
|
+
* `bedrock.region` (which is for Bedrock API calls, not AWS CLI calls).
|
|
59
75
|
*/
|
|
60
76
|
defaultRegion: z.string().optional(),
|
|
61
77
|
/** Max reasoning/tool-use steps before the agent must conclude. */
|
|
62
78
|
maxSteps: z.number().int().min(1).max(50).default(15),
|
|
63
|
-
/** All logging knobs
|
|
79
|
+
/** All logging knobs. */
|
|
64
80
|
logging: LoggingSchema,
|
|
65
81
|
/**
|
|
66
|
-
* Prompt caching. When true, the
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* this flag is silently ignored for that provider. Default true: most
|
|
72
|
-
* users invoke `aca` more than once every 5 minutes, so the cache pays
|
|
73
|
-
* for itself quickly; users running it rarely can disable it to avoid
|
|
74
|
-
* the small cache-write premium on each first call.
|
|
82
|
+
* Prompt caching. When true, the cacheable prefix is marked so providers
|
|
83
|
+
* that support it can cache hits cheaply (~10% of normal input tokens
|
|
84
|
+
* on Anthropic and Bedrock-via-Anthropic). OpenAI auto-caches large
|
|
85
|
+
* prompts and ignores this flag. Google Gemini's caching API isn't
|
|
86
|
+
* wired up yet; this flag is silently ignored for that provider.
|
|
75
87
|
*/
|
|
76
88
|
caching: z.boolean().default(true),
|
|
77
89
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* off, or vice versa. Overridable per-run with --verbose.
|
|
90
|
+
* Echo reasoning to stderr in real time. Independent of
|
|
91
|
+
* `logging.reasoningLog`. Override per-run with --verbose.
|
|
81
92
|
*/
|
|
82
93
|
verbose: z.boolean().default(false),
|
|
83
94
|
/** Auto-approval policy for command/script execution. */
|
|
@@ -90,25 +101,97 @@ export const ConfigSchema = z.object({
|
|
|
90
101
|
})
|
|
91
102
|
.default({ readOnly: true, all: false }),
|
|
92
103
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* `--interactive` / `-i` CLI flag — useful in rare edge cases where the
|
|
96
|
-
* pattern-based auto-detection misses a command that needs a TTY. Almost
|
|
97
|
-
* always you want to leave this unset and rely on either the CLI flag for
|
|
98
|
-
* one-off invocations or the per-tool-call override the agent can set.
|
|
104
|
+
* Force every AWS CLI command into interactive (TTY) mode. Almost always
|
|
105
|
+
* leave this unset and use the --interactive / -i CLI flag instead.
|
|
99
106
|
*/
|
|
100
107
|
forceInteractive: z.boolean().default(false),
|
|
101
|
-
/**
|
|
108
|
+
/** Max history entries kept in memory. */
|
|
102
109
|
historyLimit: z.number().int().min(0).default(200),
|
|
103
110
|
/**
|
|
104
|
-
* Directory where
|
|
105
|
-
*
|
|
111
|
+
* Directory where generated bash scripts are saved when the user picks
|
|
112
|
+
* "Save to disk" at the prompt. Defaults to
|
|
106
113
|
* $XDG_DATA_HOME/aws-cli-agent/scripts.
|
|
107
114
|
*/
|
|
108
115
|
scriptFolder: z.string().optional(),
|
|
109
116
|
});
|
|
117
|
+
/**
|
|
118
|
+
* Map of provider → label used in error messages and the migration hint.
|
|
119
|
+
* Kept here (not in providers.ts) so config-load errors don't depend on
|
|
120
|
+
* the provider module.
|
|
121
|
+
*/
|
|
122
|
+
const PROVIDER_LABELS = {
|
|
123
|
+
anthropic: 'anthropic',
|
|
124
|
+
openai: 'openai',
|
|
125
|
+
google: 'google',
|
|
126
|
+
bedrock: 'bedrock',
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Resolve the active provider's model. The schema marks `model` optional
|
|
130
|
+
* per-block so that we can produce a single coherent error message in
|
|
131
|
+
* `validateActiveProvider` rather than zod's multi-issue tree. Call this
|
|
132
|
+
* only after validateActiveProvider has passed.
|
|
133
|
+
*/
|
|
134
|
+
export function getActiveModel(config) {
|
|
135
|
+
const block = config[config.provider];
|
|
136
|
+
// model is guaranteed by validateActiveProvider.
|
|
137
|
+
return block.model;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Strict post-parse validation for the active provider's block. The active
|
|
141
|
+
* provider's block must exist and must contain a `model`. Pre-1.0 we treat
|
|
142
|
+
* this as a hard error rather than scaffolding defaults, so the user always
|
|
143
|
+
* knows exactly what's being called and at what cost.
|
|
144
|
+
*
|
|
145
|
+
* Call this from code paths that actually run the agent — the `run` command.
|
|
146
|
+
* Subcommands that don't need a provider (`paths`, `config`, `history`)
|
|
147
|
+
* skip this check, so a user with no config file can still use them.
|
|
148
|
+
*/
|
|
149
|
+
export function validateActiveProvider(config) {
|
|
150
|
+
const active = config.provider;
|
|
151
|
+
const block = config[active];
|
|
152
|
+
if (!block) {
|
|
153
|
+
throw new Error(`config.provider is "${active}" but no top-level "${PROVIDER_LABELS[active]}" ` +
|
|
154
|
+
`block was found. At minimum add: ` +
|
|
155
|
+
`{ "${active}": { "model": "<model-id>" } }. Run \`aca config\` to see a ` +
|
|
156
|
+
`working default scaffold.`);
|
|
157
|
+
}
|
|
158
|
+
if (!block.model) {
|
|
159
|
+
throw new Error(`config.${active}.model is required. Set it to the model identifier you ` +
|
|
160
|
+
`want to use (e.g. "claude-sonnet-4-6" for anthropic).`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Detect the pre-0.6 config shape and produce a helpful migration error
|
|
165
|
+
* rather than the cryptic "Required" zod failures the user would otherwise
|
|
166
|
+
* get. Heuristic: a top-level `model` or top-level `apiKeyEnv` is a strong
|
|
167
|
+
* signal of an old config, since neither key exists in the new schema.
|
|
168
|
+
*/
|
|
169
|
+
function detectLegacyShape(raw) {
|
|
170
|
+
if (typeof raw !== 'object' || raw === null)
|
|
171
|
+
return;
|
|
172
|
+
const obj = raw;
|
|
173
|
+
if ('model' in obj || 'apiKeyEnv' in obj) {
|
|
174
|
+
throw new Error(`Config file uses the pre-0.6.0 shape (top-level "model" / "apiKeyEnv" / ` +
|
|
175
|
+
`flat "bedrock" block). The new shape moves these into per-provider blocks:\n` +
|
|
176
|
+
`\n` +
|
|
177
|
+
` Old: New:\n` +
|
|
178
|
+
` "provider": "anthropic", "provider": "anthropic",\n` +
|
|
179
|
+
` "model": "claude-...", "anthropic": {\n` +
|
|
180
|
+
` "apiKeyEnv": "MY_KEY" "model": "claude-...",\n` +
|
|
181
|
+
` "apiKeyEnv": "MY_KEY"\n` +
|
|
182
|
+
` }\n` +
|
|
183
|
+
`\n` +
|
|
184
|
+
`For bedrock, move the existing "region"/"profile" alongside a new\n` +
|
|
185
|
+
`"model" field, all under the "bedrock" block.\n` +
|
|
186
|
+
`\n` +
|
|
187
|
+
`To start fresh: rename your old config, run \`aca config\`, then copy values across.`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
110
190
|
export function loadConfig() {
|
|
111
191
|
if (!fs.existsSync(FILES.config)) {
|
|
192
|
+
// No file = pure defaults. Strict validation is deferred to callers
|
|
193
|
+
// who actually need the provider (the `run` command), so subcommands
|
|
194
|
+
// like `paths`, `config`, `history` work without a config file.
|
|
112
195
|
return ConfigSchema.parse({});
|
|
113
196
|
}
|
|
114
197
|
let raw;
|
|
@@ -118,14 +201,41 @@ export function loadConfig() {
|
|
|
118
201
|
catch (err) {
|
|
119
202
|
throw new Error(`Config file at ${FILES.config} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
120
203
|
}
|
|
204
|
+
detectLegacyShape(raw);
|
|
121
205
|
return ConfigSchema.parse(raw);
|
|
122
206
|
}
|
|
123
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Write a default config file if none exists. Scaffolds only the active
|
|
209
|
+
* provider's block (just `model`), deliberately not creating slots for
|
|
210
|
+
* other providers (less to read) and not scaffolding `apiKey` (less
|
|
211
|
+
* temptation to put secrets on disk).
|
|
212
|
+
*
|
|
213
|
+
* Sets mode 0600 on the file. This doesn't protect against a user editing
|
|
214
|
+
* with `cp` or moving the file later, but ensures that the file as we
|
|
215
|
+
* create it isn't world-readable.
|
|
216
|
+
*/
|
|
124
217
|
export function writeDefaultConfig() {
|
|
125
218
|
fs.mkdirSync(PATHS.config, { recursive: true });
|
|
126
219
|
if (!fs.existsSync(FILES.config)) {
|
|
127
|
-
const defaults =
|
|
128
|
-
|
|
220
|
+
const defaults = {
|
|
221
|
+
provider: 'anthropic',
|
|
222
|
+
anthropic: { model: 'claude-sonnet-4-6' },
|
|
223
|
+
maxSteps: 15,
|
|
224
|
+
logging: {
|
|
225
|
+
level: 'error',
|
|
226
|
+
auditLog: true,
|
|
227
|
+
reasoningLog: false,
|
|
228
|
+
usageLog: true,
|
|
229
|
+
},
|
|
230
|
+
caching: true,
|
|
231
|
+
verbose: false,
|
|
232
|
+
autoApprove: { readOnly: true, all: false },
|
|
233
|
+
forceInteractive: false,
|
|
234
|
+
historyLimit: 200,
|
|
235
|
+
};
|
|
236
|
+
fs.writeFileSync(FILES.config, JSON.stringify(defaults, null, 2) + '\n', {
|
|
237
|
+
mode: 0o600,
|
|
238
|
+
});
|
|
129
239
|
}
|
|
130
240
|
return FILES.config;
|
|
131
241
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -47,11 +47,33 @@ export declare class FatalAwsCliError extends Error {
|
|
|
47
47
|
*/
|
|
48
48
|
export declare const FATAL_AWS_EXIT_CODES: Set<number>;
|
|
49
49
|
/**
|
|
50
|
-
* Wrap an Inquirer prompt
|
|
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.
|
|
50
|
+
* Wrap an Inquirer prompt call so that:
|
|
54
51
|
*
|
|
55
|
-
*
|
|
52
|
+
* 1. The prompt's question text renders on stderr, not stdout. Critical
|
|
53
|
+
* for `aca "..." > file.txt` — without this, the prompt would be
|
|
54
|
+
* silently swallowed into the redirected file while the user stared
|
|
55
|
+
* at a frozen terminal waiting for an invisible question.
|
|
56
|
+
*
|
|
57
|
+
* 2. Ctrl-C (which Inquirer reports as `ExitPromptError`) becomes our
|
|
58
|
+
* `UserCancelledError` sentinel. The Inquirer error class isn't
|
|
59
|
+
* easily importable, so we detect by `.name`.
|
|
60
|
+
*
|
|
61
|
+
* The `output` option lives on Inquirer's `context` parameter (the second
|
|
62
|
+
* positional argument), NOT on the config object. Spreading it into the
|
|
63
|
+
* config silently does nothing — TypeScript accepts the extra property,
|
|
64
|
+
* but at runtime Inquirer ignores it. Pass the prompt as a thunk so we
|
|
65
|
+
* can inject the context at the call site:
|
|
66
|
+
*
|
|
67
|
+
* const ok = await wrapPrompt((ctx) =>
|
|
68
|
+
* confirm({ message: 'Execute?', default: true }, ctx),
|
|
69
|
+
* );
|
|
70
|
+
*
|
|
71
|
+
* The thunk pattern is slightly more verbose than `wrapPrompt(confirm(...))`
|
|
72
|
+
* was, but it's the only shape that lets us centralize the context
|
|
73
|
+
* injection. Forgetting to pass `ctx` through to the Inquirer call is
|
|
74
|
+
* impossible to do silently — TypeScript will infer ctx's type and lint
|
|
75
|
+
* unused parameters.
|
|
56
76
|
*/
|
|
57
|
-
export declare function wrapPrompt<T>(
|
|
77
|
+
export declare function wrapPrompt<T>(factory: (ctx: {
|
|
78
|
+
output: NodeJS.WritableStream;
|
|
79
|
+
}) => Promise<T>): Promise<T>;
|
package/dist/errors.js
CHANGED
|
@@ -56,16 +56,36 @@ export class FatalAwsCliError extends Error {
|
|
|
56
56
|
*/
|
|
57
57
|
export const FATAL_AWS_EXIT_CODES = new Set([252, 253, 254, 255]);
|
|
58
58
|
/**
|
|
59
|
-
* Wrap an Inquirer prompt
|
|
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.
|
|
59
|
+
* Wrap an Inquirer prompt call so that:
|
|
63
60
|
*
|
|
64
|
-
*
|
|
61
|
+
* 1. The prompt's question text renders on stderr, not stdout. Critical
|
|
62
|
+
* for `aca "..." > file.txt` — without this, the prompt would be
|
|
63
|
+
* silently swallowed into the redirected file while the user stared
|
|
64
|
+
* at a frozen terminal waiting for an invisible question.
|
|
65
|
+
*
|
|
66
|
+
* 2. Ctrl-C (which Inquirer reports as `ExitPromptError`) becomes our
|
|
67
|
+
* `UserCancelledError` sentinel. The Inquirer error class isn't
|
|
68
|
+
* easily importable, so we detect by `.name`.
|
|
69
|
+
*
|
|
70
|
+
* The `output` option lives on Inquirer's `context` parameter (the second
|
|
71
|
+
* positional argument), NOT on the config object. Spreading it into the
|
|
72
|
+
* config silently does nothing — TypeScript accepts the extra property,
|
|
73
|
+
* but at runtime Inquirer ignores it. Pass the prompt as a thunk so we
|
|
74
|
+
* can inject the context at the call site:
|
|
75
|
+
*
|
|
76
|
+
* const ok = await wrapPrompt((ctx) =>
|
|
77
|
+
* confirm({ message: 'Execute?', default: true }, ctx),
|
|
78
|
+
* );
|
|
79
|
+
*
|
|
80
|
+
* The thunk pattern is slightly more verbose than `wrapPrompt(confirm(...))`
|
|
81
|
+
* was, but it's the only shape that lets us centralize the context
|
|
82
|
+
* injection. Forgetting to pass `ctx` through to the Inquirer call is
|
|
83
|
+
* impossible to do silently — TypeScript will infer ctx's type and lint
|
|
84
|
+
* unused parameters.
|
|
65
85
|
*/
|
|
66
|
-
export async function wrapPrompt(
|
|
86
|
+
export async function wrapPrompt(factory) {
|
|
67
87
|
try {
|
|
68
|
-
return await
|
|
88
|
+
return await factory({ output: process.stderr });
|
|
69
89
|
}
|
|
70
90
|
catch (err) {
|
|
71
91
|
if (err instanceof Error && err.name === 'ExitPromptError') {
|
package/dist/providers.d.ts
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
import type { LanguageModel } from 'ai';
|
|
2
2
|
import type { Config } from './config.js';
|
|
3
|
+
import type { Logger } from './logger.js';
|
|
3
4
|
/**
|
|
4
5
|
* Build a LanguageModel from config.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Per-provider config lives under the matching top-level block:
|
|
8
|
+
* config.anthropic.{model, apiKey, apiKeyEnv}
|
|
9
|
+
* config.openai.{model, apiKey, apiKeyEnv}
|
|
10
|
+
* config.google.{model, apiKey, apiKeyEnv}
|
|
11
|
+
* config.bedrock.{model, region, profile}
|
|
9
12
|
*
|
|
10
|
-
* For
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* For anthropic / openai / google, API key resolution order (per-provider):
|
|
14
|
+
* 1. Env var named by `<provider>.apiKeyEnv` if set in config
|
|
15
|
+
* 2. Default env var for the provider (ANTHROPIC_API_KEY etc.)
|
|
16
|
+
* 3. `<provider>.apiKey` from config (last resort — persists to disk)
|
|
17
|
+
* 4. Throw with all options listed
|
|
18
|
+
*
|
|
19
|
+
* For bedrock: no API key. The AWS credential chain (env vars, AWS_PROFILE,
|
|
20
|
+
* ~/.aws/credentials, SSO, IMDS, container roles) is used. `bedrock.profile`
|
|
21
|
+
* optionally pins a specific named profile — useful when the account hosting
|
|
22
|
+
* Bedrock model access is different from the accounts the agent operates
|
|
23
|
+
* against.
|
|
24
|
+
*
|
|
25
|
+
* The `logger` parameter is used to emit a debug-level note when the key
|
|
26
|
+
* resolves from config instead of env, so an investigator can see "this run
|
|
27
|
+
* read the key from disk" in general.log. The key itself is never logged.
|
|
15
28
|
*/
|
|
16
|
-
export declare function createModel(config: Config): LanguageModel;
|
|
29
|
+
export declare function createModel(config: Config, logger?: Logger): LanguageModel;
|
package/dist/providers.js
CHANGED
|
@@ -3,6 +3,10 @@ import { createOpenAI } from '@ai-sdk/openai';
|
|
|
3
3
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
4
4
|
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
|
|
5
5
|
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
|
|
6
|
+
/**
|
|
7
|
+
* Map provider → default env-var name. Used as the second fallback in the
|
|
8
|
+
* resolution chain (see `requireKey`).
|
|
9
|
+
*/
|
|
6
10
|
const DEFAULT_KEY_ENV = {
|
|
7
11
|
anthropic: 'ANTHROPIC_API_KEY',
|
|
8
12
|
openai: 'OPENAI_API_KEY',
|
|
@@ -11,49 +15,100 @@ const DEFAULT_KEY_ENV = {
|
|
|
11
15
|
/**
|
|
12
16
|
* Build a LanguageModel from config.
|
|
13
17
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
18
|
+
* Per-provider config lives under the matching top-level block:
|
|
19
|
+
* config.anthropic.{model, apiKey, apiKeyEnv}
|
|
20
|
+
* config.openai.{model, apiKey, apiKeyEnv}
|
|
21
|
+
* config.google.{model, apiKey, apiKeyEnv}
|
|
22
|
+
* config.bedrock.{model, region, profile}
|
|
23
|
+
*
|
|
24
|
+
* For anthropic / openai / google, API key resolution order (per-provider):
|
|
25
|
+
* 1. Env var named by `<provider>.apiKeyEnv` if set in config
|
|
26
|
+
* 2. Default env var for the provider (ANTHROPIC_API_KEY etc.)
|
|
27
|
+
* 3. `<provider>.apiKey` from config (last resort — persists to disk)
|
|
28
|
+
* 4. Throw with all options listed
|
|
29
|
+
*
|
|
30
|
+
* For bedrock: no API key. The AWS credential chain (env vars, AWS_PROFILE,
|
|
31
|
+
* ~/.aws/credentials, SSO, IMDS, container roles) is used. `bedrock.profile`
|
|
32
|
+
* optionally pins a specific named profile — useful when the account hosting
|
|
33
|
+
* Bedrock model access is different from the accounts the agent operates
|
|
34
|
+
* against.
|
|
17
35
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* which is useful when the account hosting Bedrock model access is different
|
|
22
|
-
* from the accounts the agent operates against.
|
|
36
|
+
* The `logger` parameter is used to emit a debug-level note when the key
|
|
37
|
+
* resolves from config instead of env, so an investigator can see "this run
|
|
38
|
+
* read the key from disk" in general.log. The key itself is never logged.
|
|
23
39
|
*/
|
|
24
|
-
export function createModel(config) {
|
|
40
|
+
export function createModel(config, logger) {
|
|
25
41
|
switch (config.provider) {
|
|
26
42
|
case 'anthropic': {
|
|
27
|
-
const
|
|
28
|
-
|
|
43
|
+
const block = config.anthropic; // validated upstream
|
|
44
|
+
const apiKey = requireKey('anthropic', block.apiKey, block.apiKeyEnv, logger);
|
|
45
|
+
return createAnthropic({ apiKey })(block.model);
|
|
29
46
|
}
|
|
30
47
|
case 'openai': {
|
|
31
|
-
const
|
|
32
|
-
|
|
48
|
+
const block = config.openai;
|
|
49
|
+
const apiKey = requireKey('openai', block.apiKey, block.apiKeyEnv, logger);
|
|
50
|
+
return createOpenAI({ apiKey })(block.model);
|
|
33
51
|
}
|
|
34
52
|
case 'google': {
|
|
35
|
-
const
|
|
36
|
-
|
|
53
|
+
const block = config.google;
|
|
54
|
+
const apiKey = requireKey('google', block.apiKey, block.apiKeyEnv, logger);
|
|
55
|
+
return createGoogleGenerativeAI({ apiKey })(block.model);
|
|
37
56
|
}
|
|
38
57
|
case 'bedrock': {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
process.env.AWS_DEFAULT_REGION;
|
|
58
|
+
const block = config.bedrock;
|
|
59
|
+
const region = block.region ?? process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION;
|
|
42
60
|
if (!region) {
|
|
43
61
|
throw new Error('Bedrock requires a region. Set "bedrock.region" in config or AWS_REGION env var.');
|
|
44
62
|
}
|
|
45
|
-
const credentialProvider =
|
|
46
|
-
? fromNodeProviderChain({ profile:
|
|
63
|
+
const credentialProvider = block.profile
|
|
64
|
+
? fromNodeProviderChain({ profile: block.profile })
|
|
47
65
|
: fromNodeProviderChain();
|
|
48
|
-
return createAmazonBedrock({ region, credentialProvider })(
|
|
66
|
+
return createAmazonBedrock({ region, credentialProvider })(block.model);
|
|
49
67
|
}
|
|
50
68
|
}
|
|
51
69
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Three-tier API key resolution. Returns the key (never logs it). Emits a
|
|
72
|
+
* debug-level note when the key came from config rather than env, so an
|
|
73
|
+
* investigator looking at general.log can see what happened.
|
|
74
|
+
*
|
|
75
|
+
* When `apiKeyEnv` is set in config but the named env var is empty, we emit
|
|
76
|
+
* a warning to stderr and fall through to the default env var. The warning
|
|
77
|
+
* matters because `apiKeyEnv` is an explicit user instruction ("read the
|
|
78
|
+
* key from THIS variable") — silently using a different source could send
|
|
79
|
+
* the wrong account's request to the model provider.
|
|
80
|
+
*/
|
|
81
|
+
function requireKey(provider, configKey, configKeyEnv, logger) {
|
|
82
|
+
// 1. Custom env var name from config takes precedence — when it's set.
|
|
83
|
+
if (configKeyEnv) {
|
|
84
|
+
const v = process.env[configKeyEnv];
|
|
85
|
+
if (v)
|
|
86
|
+
return v;
|
|
87
|
+
// The user told us where to look, but the variable is empty. This is
|
|
88
|
+
// almost always a mistake (typo'd var name, forgot to export it,
|
|
89
|
+
// wrong shell). Surface it loudly so they fix it; then fall through
|
|
90
|
+
// to the default env var so the run can still succeed if that's set.
|
|
91
|
+
process.stderr.write(`Warning: config.${provider}.apiKeyEnv names "${configKeyEnv}" but ` +
|
|
92
|
+
`that environment variable is not set. Falling back to the default ` +
|
|
93
|
+
`(${DEFAULT_KEY_ENV[provider]}) or config.${provider}.apiKey.\n`);
|
|
94
|
+
logger?.warn(`${provider}: configured apiKeyEnv "${configKeyEnv}" is not set in the environment.`);
|
|
95
|
+
}
|
|
96
|
+
// 2. Default env var for the provider.
|
|
97
|
+
const defaultEnv = DEFAULT_KEY_ENV[provider];
|
|
98
|
+
const fromDefaultEnv = process.env[defaultEnv];
|
|
99
|
+
if (fromDefaultEnv)
|
|
100
|
+
return fromDefaultEnv;
|
|
101
|
+
// 3. Config-stored key — last resort. Note it.
|
|
102
|
+
if (configKey) {
|
|
103
|
+
logger?.debug(`${provider}: API key loaded from config file (no env var set). ` +
|
|
104
|
+
`Prefer ${configKeyEnv ?? defaultEnv} env var for production use.`);
|
|
105
|
+
return configKey;
|
|
57
106
|
}
|
|
58
|
-
|
|
107
|
+
// 4. Nothing.
|
|
108
|
+
throw new Error(`No API key found for provider "${provider}". Set one of:\n` +
|
|
109
|
+
(configKeyEnv
|
|
110
|
+
? ` - env var ${configKeyEnv} (named by config.${provider}.apiKeyEnv)\n`
|
|
111
|
+
: '') +
|
|
112
|
+
` - env var ${defaultEnv} (default for ${provider})\n` +
|
|
113
|
+
` - config.${provider}.apiKey (persists to disk — env var preferred)`);
|
|
59
114
|
}
|
package/dist/tools/aws-cli.d.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import type { Logger } from '../logger.js';
|
|
2
2
|
import type { Config } from '../config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Read-only verb patterns. Used both to decide whether an AWS CLI call may
|
|
5
|
+
* auto-approve, and (since 0.7.0) to syntax-highlight bash scripts in the
|
|
6
|
+
* approval display — read-only verbs render in light blue, mutating in yellow.
|
|
7
|
+
*
|
|
8
|
+
* Exported so bash.ts can apply the same classification consistently. If
|
|
9
|
+
* you add a verb here, the highlighter picks it up automatically.
|
|
10
|
+
*/
|
|
11
|
+
export declare const READ_ONLY_VERBS: RegExp[];
|
|
12
|
+
/**
|
|
13
|
+
* Full-command patterns that are read-only but don't match a verb prefix
|
|
14
|
+
* (e.g. `s3 ls`, `sts get-caller-identity` where the resource type isn't a
|
|
15
|
+
* standard verb). Same usage as READ_ONLY_VERBS.
|
|
16
|
+
*/
|
|
17
|
+
export declare const READ_ONLY_FULL: RegExp[];
|
|
3
18
|
export declare function awsCliTool(opts: {
|
|
4
19
|
logger: Logger;
|
|
5
20
|
config: Config;
|
package/dist/tools/aws-cli.js
CHANGED
|
@@ -4,7 +4,15 @@ import { z } from 'zod';
|
|
|
4
4
|
import { confirm } from '@inquirer/prompts';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { FATAL_AWS_EXIT_CODES, FatalAwsCliError, UserCancelledError, wrapPrompt } from '../errors.js';
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Read-only verb patterns. Used both to decide whether an AWS CLI call may
|
|
9
|
+
* auto-approve, and (since 0.7.0) to syntax-highlight bash scripts in the
|
|
10
|
+
* approval display — read-only verbs render in light blue, mutating in yellow.
|
|
11
|
+
*
|
|
12
|
+
* Exported so bash.ts can apply the same classification consistently. If
|
|
13
|
+
* you add a verb here, the highlighter picks it up automatically.
|
|
14
|
+
*/
|
|
15
|
+
export const READ_ONLY_VERBS = [
|
|
8
16
|
/^describe-/,
|
|
9
17
|
/^list-/,
|
|
10
18
|
/^get-/,
|
|
@@ -15,7 +23,12 @@ const READ_ONLY_VERBS = [
|
|
|
15
23
|
/^scan$/,
|
|
16
24
|
/^query$/,
|
|
17
25
|
];
|
|
18
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Full-command patterns that are read-only but don't match a verb prefix
|
|
28
|
+
* (e.g. `s3 ls`, `sts get-caller-identity` where the resource type isn't a
|
|
29
|
+
* standard verb). Same usage as READ_ONLY_VERBS.
|
|
30
|
+
*/
|
|
31
|
+
export const READ_ONLY_FULL = [
|
|
19
32
|
/^s3\s+ls(\s|$)/,
|
|
20
33
|
];
|
|
21
34
|
/**
|
|
@@ -108,14 +121,40 @@ function runCaptured(cmd, args) {
|
|
|
108
121
|
*/
|
|
109
122
|
function runInteractive(cmd, args) {
|
|
110
123
|
return new Promise((resolve, reject) => {
|
|
124
|
+
// Detach our SIGINT handler for the duration of the child's lifetime.
|
|
125
|
+
// Background: when the user presses Ctrl-C inside an SSM session, the
|
|
126
|
+
// OS delivers SIGINT to the whole foreground process group — both the
|
|
127
|
+
// aws CLI child AND this Node process. Node's default SIGINT handler
|
|
128
|
+
// would terminate us, which tears down the inherited stdin file
|
|
129
|
+
// descriptor before the aws CLI has finished its own cleanup. The
|
|
130
|
+
// result is the aws CLI seeing "input/output error" on /dev/stdin and
|
|
131
|
+
// printing a confusing error to the user.
|
|
132
|
+
//
|
|
133
|
+
// By installing a no-op SIGINT listener, we override Node's default
|
|
134
|
+
// behavior: Node sees the signal as "handled" and doesn't terminate
|
|
135
|
+
// us. The aws CLI subprocess gets the signal too (same process group)
|
|
136
|
+
// and runs its own clean shutdown — closing the SSM session politely,
|
|
137
|
+
// exiting with code 0 or 130. We then continue normally.
|
|
138
|
+
//
|
|
139
|
+
// The listener is removed in `close` so SIGINT during normal agent
|
|
140
|
+
// reasoning still aborts the run as expected.
|
|
141
|
+
const noopSigint = () => {
|
|
142
|
+
/* deliberately empty — see comment above */
|
|
143
|
+
};
|
|
144
|
+
process.on('SIGINT', noopSigint);
|
|
111
145
|
const proc = spawn(cmd, args, {
|
|
112
146
|
env: process.env,
|
|
113
|
-
stdio: 'inherit', //
|
|
147
|
+
stdio: 'inherit', // child reuses parent's stdin/stdout/stderr
|
|
148
|
+
});
|
|
149
|
+
proc.on('error', (err) => {
|
|
150
|
+
process.removeListener('SIGINT', noopSigint);
|
|
151
|
+
reject(err);
|
|
152
|
+
});
|
|
153
|
+
proc.on('close', (code) => {
|
|
154
|
+
process.removeListener('SIGINT', noopSigint);
|
|
155
|
+
// We can't observe stdout/stderr — they went to the user's terminal.
|
|
156
|
+
resolve({ stdout: '', stderr: '', code: code ?? 0 });
|
|
114
157
|
});
|
|
115
|
-
proc.on('error', reject);
|
|
116
|
-
proc.on('close', (code) =>
|
|
117
|
-
// We can't observe stdout/stderr — they went to the user's terminal.
|
|
118
|
-
resolve({ stdout: '', stderr: '', code: code ?? 0 }));
|
|
119
158
|
});
|
|
120
159
|
}
|
|
121
160
|
export function awsCliTool(opts) {
|
|
@@ -165,7 +204,7 @@ export function awsCliTool(opts) {
|
|
|
165
204
|
if (useInteractive) {
|
|
166
205
|
process.stderr.write(`${chalk.bold(' Mode: ')}${chalk.yellow('interactive')} (your terminal will be connected to the command)\n`);
|
|
167
206
|
}
|
|
168
|
-
const ok = await wrapPrompt(confirm({ message: 'Execute this command?', default: true }));
|
|
207
|
+
const ok = await wrapPrompt((ctx) => confirm({ message: 'Execute this command?', default: true }, ctx));
|
|
169
208
|
if (!ok) {
|
|
170
209
|
opts.logger.warn('User declined command');
|
|
171
210
|
// Record the declined call so the agent's end-of-run logic sees
|
package/dist/tools/bash.js
CHANGED
|
@@ -8,6 +8,7 @@ import { select } from '@inquirer/prompts';
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { DEFAULT_SCRIPT_FOLDER } from '../paths.js';
|
|
10
10
|
import { wrapPrompt } from '../errors.js';
|
|
11
|
+
import { READ_ONLY_FULL, READ_ONLY_VERBS } from './aws-cli.js';
|
|
11
12
|
function runProcess(cmd, args) {
|
|
12
13
|
return new Promise((resolve, reject) => {
|
|
13
14
|
const proc = spawn(cmd, args, { env: process.env });
|
|
@@ -32,6 +33,109 @@ function indent(s, prefix) {
|
|
|
32
33
|
.map((l) => prefix + l)
|
|
33
34
|
.join('\n');
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Match an `aws <service> <verb> ...` invocation anywhere within a line.
|
|
38
|
+
* The lookbehind is intentionally permissive — it matches `aws` at the
|
|
39
|
+
* start of the line, after a pipe (`| aws ...`), after `time aws ...`,
|
|
40
|
+
* after `env VAR=val aws ...`, etc. We don't try to be a full shell
|
|
41
|
+
* parser: that's overkill for a syntax-highlight feature. False positives
|
|
42
|
+
* (e.g. the literal string `"use aws ec2 ..."` inside an echo) get
|
|
43
|
+
* highlighted too, but that's preferable to false negatives — and the
|
|
44
|
+
* surrounding context usually makes them visually distinguishable.
|
|
45
|
+
*
|
|
46
|
+
* Captures three groups: service (group 1), verb (group 2), and the rest
|
|
47
|
+
* of the args up to end of line or a shell metacharacter that ends a
|
|
48
|
+
* command (group 3). We stop the arg span at `|`, `;`, `&`, `)`, and `>` /
|
|
49
|
+
* `<` redirections so we don't highlight half of the next pipeline stage.
|
|
50
|
+
*/
|
|
51
|
+
const AWS_CALL_RE = /\baws\s+([a-z][a-z0-9-]*)\s+([a-z][a-z0-9-]*)((?:\s+[^|;&)<>\n]*)?)/g;
|
|
52
|
+
/**
|
|
53
|
+
* Decide whether an `aws <service> <verb>` invocation is read-only. Uses
|
|
54
|
+
* the same classification as the per-command auto-approve path so the
|
|
55
|
+
* highlighting matches the runtime behavior: if the highlight is light
|
|
56
|
+
* blue, the command would auto-approve with `autoApprove.readOnly: true`
|
|
57
|
+
* if it were a standalone tool call.
|
|
58
|
+
*/
|
|
59
|
+
function isAwsCallReadOnly(service, verb, restOfArgs) {
|
|
60
|
+
// READ_ONLY_FULL patterns match against `service verb [args...]`.
|
|
61
|
+
// We pass the same shape to mirror runtime behavior.
|
|
62
|
+
const full = `${service} ${verb}${restOfArgs}`;
|
|
63
|
+
if (READ_ONLY_FULL.some((re) => re.test(full)))
|
|
64
|
+
return true;
|
|
65
|
+
if (READ_ONLY_VERBS.some((re) => re.test(verb)))
|
|
66
|
+
return true;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Color the AWS CLI invocations inside a script. Read-only calls
|
|
71
|
+
* (describe-, list-, get-, etc.) render in light blue; mutating calls
|
|
72
|
+
* (delete-, terminate-, create-, etc.) render in yellow. Non-AWS portions
|
|
73
|
+
* of each line stay in the script body's default green wrap.
|
|
74
|
+
*
|
|
75
|
+
* Implementation note: chalk colors don't nest the way you'd hope. If we
|
|
76
|
+
* wrapped the whole script in `chalk.green()` and then embedded blue/yellow
|
|
77
|
+
* spans inside it, the inner spans' closing reset (\u001b[39m) would
|
|
78
|
+
* disable the green for the remainder of the string. To avoid that, we
|
|
79
|
+
* render each line piece by piece, explicitly re-applying green to the
|
|
80
|
+
* non-highlighted spans.
|
|
81
|
+
*/
|
|
82
|
+
function highlightAwsCalls(script) {
|
|
83
|
+
return script
|
|
84
|
+
.split('\n')
|
|
85
|
+
.map((line) => {
|
|
86
|
+
// Walk the line in pieces, emitting green for plain text and a
|
|
87
|
+
// distinct color for each AWS invocation. We use the regex's
|
|
88
|
+
// exec-loop position tracking to splice the line.
|
|
89
|
+
AWS_CALL_RE.lastIndex = 0;
|
|
90
|
+
let out = '';
|
|
91
|
+
let cursor = 0;
|
|
92
|
+
let m;
|
|
93
|
+
while ((m = AWS_CALL_RE.exec(line)) !== null) {
|
|
94
|
+
// The `aws` keyword itself isn't in the capture groups — find it
|
|
95
|
+
// by scanning backward from the service position.
|
|
96
|
+
const matchStart = m.index;
|
|
97
|
+
const service = m[1];
|
|
98
|
+
const verb = m[2];
|
|
99
|
+
const restOfArgs = m[3] ?? '';
|
|
100
|
+
const readOnly = isAwsCallReadOnly(service, verb, restOfArgs);
|
|
101
|
+
// Pre-`aws` text (e.g. `| `, `time `, or empty for start-of-line)
|
|
102
|
+
// stays green.
|
|
103
|
+
if (matchStart > cursor) {
|
|
104
|
+
out += chalk.green(line.slice(cursor, matchStart));
|
|
105
|
+
}
|
|
106
|
+
// The `aws <service> <verb>` triple gets the distinct color.
|
|
107
|
+
// restOfArgs (the flags and values) stays green — flags are the
|
|
108
|
+
// boring part, the verb is what the user needs to verify.
|
|
109
|
+
// Inverse (swaps fg/bg) produces a "highlighter strip" look: the
|
|
110
|
+
// background takes the color and the text shows through in the
|
|
111
|
+
// terminal's default foreground. Much more visible against the
|
|
112
|
+
// green script body than colored text alone would be — the eye
|
|
113
|
+
// snaps to a block of color faster than to a colored word.
|
|
114
|
+
// Green strip for read-only (matches the safe/discovery feel of
|
|
115
|
+
// the surrounding green script body); red strip for mutating
|
|
116
|
+
// (the classic "stop and look" signal).
|
|
117
|
+
const highlight = readOnly
|
|
118
|
+
? chalk.inverse.blueBright
|
|
119
|
+
: chalk.inverse.yellowBright;
|
|
120
|
+
out += highlight(`aws ${service} ${verb}`);
|
|
121
|
+
if (restOfArgs.length > 0) {
|
|
122
|
+
out += chalk.green(restOfArgs);
|
|
123
|
+
}
|
|
124
|
+
cursor = matchStart + m[0].length;
|
|
125
|
+
}
|
|
126
|
+
// Trailing text after the last match stays green.
|
|
127
|
+
if (cursor < line.length) {
|
|
128
|
+
out += chalk.green(line.slice(cursor));
|
|
129
|
+
}
|
|
130
|
+
// If no AWS call was found in the line, fall through with the whole
|
|
131
|
+
// line in green — same as the old uniform-green behavior.
|
|
132
|
+
if (out === '') {
|
|
133
|
+
out = chalk.green(line);
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
})
|
|
137
|
+
.join('\n');
|
|
138
|
+
}
|
|
35
139
|
/**
|
|
36
140
|
* Compute a filesystem-friendly filename for a saved script.
|
|
37
141
|
* Combines a timestamp (so files sort chronologically) with a short slug
|
|
@@ -64,7 +168,11 @@ export function bashScriptTool(opts) {
|
|
|
64
168
|
process.stderr.write('\n');
|
|
65
169
|
process.stderr.write(`${chalk.bold(' Reason: ')}${purpose}\n`);
|
|
66
170
|
process.stderr.write(`${chalk.bold(' Script:')}\n`);
|
|
67
|
-
|
|
171
|
+
// Render the script with AWS calls highlighted. Read-only calls
|
|
172
|
+
// (describe-, list-, get-, etc.) render in light blue; mutating calls
|
|
173
|
+
// (delete-, terminate-, create-, etc.) render in yellow. Everything
|
|
174
|
+
// else stays green. See highlightAwsCalls for the parser caveats.
|
|
175
|
+
process.stderr.write(highlightAwsCalls(indent(script, ' ')) + '\n');
|
|
68
176
|
// The save-to-disk option respects the configured folder, or falls back
|
|
69
177
|
// to the XDG default. Compute the would-be path *before* prompting so
|
|
70
178
|
// the user can see exactly where it'll land.
|
|
@@ -75,7 +183,7 @@ export function bashScriptTool(opts) {
|
|
|
75
183
|
// — auto-approving them would defeat a primary safety boundary. The
|
|
76
184
|
// autoApprove flag remains in effect for individual aws CLI commands
|
|
77
185
|
// (where read-only is a meaningful and enforceable category).
|
|
78
|
-
const action = await wrapPrompt(select({
|
|
186
|
+
const action = await wrapPrompt((ctx) => select({
|
|
79
187
|
message: 'What would you like to do with this script?',
|
|
80
188
|
choices: [
|
|
81
189
|
{ value: 'execute', name: 'Execute now' },
|
|
@@ -83,7 +191,7 @@ export function bashScriptTool(opts) {
|
|
|
83
191
|
{ value: 'cancel', name: 'Cancel' },
|
|
84
192
|
],
|
|
85
193
|
default: 'execute',
|
|
86
|
-
}));
|
|
194
|
+
}, ctx));
|
|
87
195
|
if (action === 'cancel') {
|
|
88
196
|
opts.logger.warn('User cancelled script');
|
|
89
197
|
// Record the cancelled call so the agent's end-of-run logic sees
|
package/dist/tools/prompt.js
CHANGED
|
@@ -45,28 +45,31 @@ async function askOne(q, logger) {
|
|
|
45
45
|
if (!q.choices || q.choices.length === 0) {
|
|
46
46
|
throw new Error('kind="choice" requires non-empty `choices`.');
|
|
47
47
|
}
|
|
48
|
-
|
|
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({
|
|
49
52
|
message: q.message,
|
|
50
|
-
choices:
|
|
53
|
+
choices: choices.map((c) => ({ value: c, name: c })),
|
|
51
54
|
default: q.defaultValue,
|
|
52
|
-
}));
|
|
55
|
+
}, ctx));
|
|
53
56
|
return answer;
|
|
54
57
|
}
|
|
55
58
|
case 'confirm': {
|
|
56
59
|
const def = (q.defaultValue ?? 'yes').toLowerCase().startsWith('y');
|
|
57
|
-
const answer = await wrapPrompt(confirm({ message: q.message, default: def }));
|
|
60
|
+
const answer = await wrapPrompt((ctx) => confirm({ message: q.message, default: def }, ctx));
|
|
58
61
|
return answer ? 'yes' : 'no';
|
|
59
62
|
}
|
|
60
63
|
case 'secret': {
|
|
61
64
|
// Inquirer's password prompt masks input. Used for short secrets like
|
|
62
65
|
// MFA codes; long-lived AWS credentials should always come from the
|
|
63
66
|
// user's profile, not be typed here.
|
|
64
|
-
const answer = await wrapPrompt(password({ message: q.message, mask: '*' }));
|
|
67
|
+
const answer = await wrapPrompt((ctx) => password({ message: q.message, mask: '*' }, ctx));
|
|
65
68
|
return answer;
|
|
66
69
|
}
|
|
67
70
|
case 'text':
|
|
68
71
|
default: {
|
|
69
|
-
const answer = await wrapPrompt(input({ message: q.message, default: q.defaultValue }));
|
|
72
|
+
const answer = await wrapPrompt((ctx) => input({ message: q.message, default: q.defaultValue }, ctx));
|
|
70
73
|
return answer;
|
|
71
74
|
}
|
|
72
75
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aws-cli-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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": "^8.
|
|
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
|
}
|