aws-cli-agent 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +250 -2
- package/README.md +60 -13
- package/dist/agent.d.ts +12 -0
- package/dist/agent.js +151 -48
- package/dist/cli.js +36 -7
- package/dist/config.d.ts +45 -4
- package/dist/config.js +157 -47
- package/dist/errors.d.ts +79 -0
- package/dist/errors.js +96 -0
- package/dist/providers.d.ts +22 -9
- package/dist/providers.js +82 -27
- package/dist/tools/aws-cli.js +66 -10
- package/dist/tools/bash.js +3 -2
- package/dist/tools/prompt.js +10 -6
- package/package.json +14 -14
package/dist/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
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
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 call so that:
|
|
51
|
+
*
|
|
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.
|
|
76
|
+
*/
|
|
77
|
+
export declare function wrapPrompt<T>(factory: (ctx: {
|
|
78
|
+
output: NodeJS.WritableStream;
|
|
79
|
+
}) => Promise<T>): Promise<T>;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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 call so that:
|
|
60
|
+
*
|
|
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.
|
|
85
|
+
*/
|
|
86
|
+
export async function wrapPrompt(factory) {
|
|
87
|
+
try {
|
|
88
|
+
return await factory({ output: process.stderr });
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (err instanceof Error && err.name === 'ExitPromptError') {
|
|
92
|
+
throw new UserCancelledError();
|
|
93
|
+
}
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
}
|
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
|
}
|