Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/next-release/enhancement-configure-67593.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "enhancement",
"category": "configure",
"description": "Suggest installing the Agent Toolkit for AWS when a supported AI coding agent is detected: interactively after ``aws configure``, ``aws configure sso``, ``aws update`` (Unix), and a first-time ``aws login`` that creates a new profile, and as a tip printed by the install scripts. The interactive prompt appears only when no AWS skills are installed and can be permanently suppressed by answering ``never``."
}
8 changes: 8 additions & 0 deletions awscli/customizations/agenttoolkit/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,11 @@ def get_detected_agents(agent_configs=None):
if agent is not None:
detected.append(agent)
return detected


def get_detected_real_agents(agent_configs=None):
return [
agent
for agent in get_detected_agents(agent_configs)
if agent.config.id != UNIVERSAL_ROW_ID
]
109 changes: 109 additions & 0 deletions awscli/customizations/agenttoolkit/hint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
"""End-of-``aws configure`` hint suggesting the Agent Toolkit wizard.

After a successful ``aws configure`` that writes profile values, offer to run
``aws configure agent-toolkit`` when a supported AI coding agent is present and
the toolkit is not already installed. The hint is interactive-only and can be
permanently suppressed.
"""

import json
import logging
import os

from awscli.customizations.agenttoolkit.agents import (
get_detected_real_agents,
)
from awscli.customizations.agenttoolkit.configure import (
ConfigureAgentToolkitCommand,
)
from awscli.customizations.prompts import yes_no_never_choice
from awscli.customizations.utils import uni_print
from awscli.utils import is_stdin_a_tty

LOG = logging.getLogger(__name__)

STATE_PATH = '~/.aws/cli/agent-toolkit/state.json'

PROMPT_TEXT = (
'Configure AWS skills and the AWS MCP server for your AI coding '
'agent(s)? [Y/n/never]: '
)


def _state_file():
return os.path.expanduser(STATE_PATH)


def _load_state():
try:
with open(_state_file()) as f:
return json.load(f)
except FileNotFoundError:
return {}
except (OSError, json.JSONDecodeError) as e:
LOG.debug('Could not read agent toolkit hint state: %s', e)
return {}


def _save_state(state):
path = _state_file()
try:
os.makedirs(os.path.dirname(path), exist_ok=True)
tmp_path = f'{path}.tmp'
with open(tmp_path, 'w') as f:
json.dump(state, f)
f.write('\n')
os.replace(tmp_path, path)
except OSError as e:
LOG.debug('Could not write agent toolkit hint state: %s', e)


def _dismiss_forever():
state = _load_state()
state['hint_dismissed'] = True
_save_state(state)


def _has_installed_skills(detected_agents):
return any(agent.get_installed_skills() for agent in detected_agents)


def _is_eligible(detected_agents):
if not is_stdin_a_tty():
return False
if _load_state().get('hint_dismissed'):
return False
if not detected_agents:
return False
if _has_installed_skills(detected_agents):
return False
return True


def maybe_prompt_agent_toolkit(session, parsed_globals, stream=None):
try:
detected_agents = get_detected_real_agents()
if not _is_eligible(detected_agents):
return
choice = yes_no_never_choice(PROMPT_TEXT)
if choice == 'never':
_dismiss_forever()
return
if choice == 'no':
return
command = ConfigureAgentToolkitCommand(session)
command([], parsed_globals)
except Exception as e:
LOG.debug('Agent toolkit hint failed: %s', e, exc_info=True)
4 changes: 4 additions & 0 deletions awscli/customizations/configure/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
from awscli.customizations.agenttoolkit.configure import (
ConfigureAgentToolkitCommand,
)
from awscli.customizations.agenttoolkit.hint import (
maybe_prompt_agent_toolkit,
)
from awscli.customizations.commands import BasicCommand
from awscli.customizations.configure.addmodel import AddModelCommand
from awscli.customizations.configure.exportcreds import (
Expand Down Expand Up @@ -193,6 +196,7 @@ def _run_main(self, parsed_args, parsed_globals):
section = profile_to_section(profile)
new_values['__section__'] = section
self._config_writer.update_config(new_values, config_filename)
maybe_prompt_agent_toolkit(self._session, parsed_globals)
return 0

def _write_out_creds_file_values(self, new_values, profile_name):
Expand Down
4 changes: 4 additions & 0 deletions awscli/customizations/configure/sso_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
from botocore.exceptions import ProfileNotFound
from botocore.useragent import register_feature_id

from awscli.customizations.agenttoolkit.hint import (
maybe_prompt_agent_toolkit,
)
from awscli.customizations.configure import (
get_section_header,
profile_to_section,
Expand Down Expand Up @@ -352,6 +355,7 @@ def _run_main(self, parsed_args, parsed_globals):

self._write_new_config(profile_name)
self._print_conclusion(configured_for_aws_credentials, profile_name)
maybe_prompt_agent_toolkit(self._session, parsed_globals)
return 0

def _prompt_for_sso_registration_args(self, verify=None):
Expand Down
11 changes: 10 additions & 1 deletion awscli/customizations/login/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
)

from awscli.compat import compat_input
from awscli.customizations.agenttoolkit.hint import (
maybe_prompt_agent_toolkit,
)
from awscli.customizations.commands import BasicCommand
from awscli.customizations.configure.writer import ConfigFileWriter
from awscli.customizations.exceptions import ConfigurationError
Expand Down Expand Up @@ -92,7 +95,8 @@ def _run_main(self, parsed_args, parsed_globals):
# If the profile specified via --profile doesn't already exist
# add it to the session so the client creation still succeeds.
# If the login is successful we'll save the profile at the end.
if profile_name not in self._session.available_profiles:
is_new_profile = profile_name not in self._session.available_profiles
if is_new_profile:
self._session._profile_map[profile_name] = {}

# Abort if the profile is already configured with a different style
Expand Down Expand Up @@ -153,6 +157,11 @@ def _run_main(self, parsed_args, parsed_globals):
f'such as "aws sts get-caller-identity --profile {profile_name}"\n'
)

# Only nudge on first-time setup (a newly created profile), not on
# routine re-auth of an existing profile.
if is_new_profile:
maybe_prompt_agent_toolkit(self._session, parsed_globals)

def accept_change_to_existing_profile_if_needed(
self, profile_name, new_session_id
):
Expand Down
21 changes: 21 additions & 0 deletions awscli/customizations/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,27 @@ def yes_no_choice(prompt):
uni_print('Invalid response. Please enter "y" or "n"\n')


def yes_no_never_choice(prompt):
"""
Prompts the user with a yes/no/never question.
Continually re-prompts for invalid selections.

:param prompt: Prompt text.
:returns: 'yes', 'no', or 'never'.
"""
while True:
response = compat_input(prompt)

if response.lower() in ('y', 'yes') or response == '':
return 'yes'
elif response.lower() in ('n', 'no'):
return 'no'
elif response.lower() == 'never':
return 'never'
else:
uni_print('Invalid response. Please enter "y", "n", or "never"\n')


def multiselect_choice(
message,
items,
Expand Down
14 changes: 14 additions & 0 deletions awscli/customizations/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
get_distribution_source,
)
from awscli.compat import is_windows
from awscli.customizations.agenttoolkit.hint import (
maybe_prompt_agent_toolkit,
)
from awscli.customizations.commands import BasicCommand
from awscli.customizations.utils import uni_print

Expand Down Expand Up @@ -95,8 +98,12 @@ def _run_main(self, parsed_args, parsed_globals):
uni_print(f"Updating AWS CLI (source: {source})\n")
self._no_color = parsed_globals.color == 'off'
self._do_update()
self._maybe_prompt_agent_toolkit(parsed_globals)
return 0

def _maybe_prompt_agent_toolkit(self, parsed_globals):
maybe_prompt_agent_toolkit(self._session, parsed_globals)

def _do_update(self):
raise NotImplementedError

Expand Down Expand Up @@ -220,6 +227,13 @@ def _do_update(self):
'update will complete shortly.\n'
)

def _maybe_prompt_agent_toolkit(self, parsed_globals):
# No-op on Windows: the update runs in a detached subprocess and the
# actual CLI update happens after this process exits, so there is no
# safe point to prompt (blocking here would hold the exe lock the
# detached-process workaround exists to avoid).
pass

def _run_install(self, cmd):
subprocess.Popen(
cmd,
Expand Down
1 change: 1 addition & 0 deletions exe/assets/install
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ main() {
create_bin_symlinks
write_install_json
echo "You can now run: $BIN_AWS_EXE --version"
echo "Tip: run 'aws configure agent-toolkit' to set up AWS skills and the AWS MCP server for your AI coding agent."
exit 0
}

Expand Down
2 changes: 2 additions & 0 deletions macpkg/scripts/postinstall
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ EOF
EOF
fi
fi

echo "Tip: run 'aws configure agent-toolkit' to set up AWS skills and the AWS MCP server for your AI coding agent."
23 changes: 23 additions & 0 deletions tests/unit/customizations/agenttoolkit/test_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

from awscli.customizations.agenttoolkit.agents import (
AGENT_CONFIGS,
UNIVERSAL_ROW_ID,
AgentConfig,
DetectedAgent,
McpConfigureAction,
get_detected_agents,
get_detected_real_agents,
)
from awscli.testutils import skip_if_windows
from tests.unit.customizations.agenttoolkit.utils import (
Expand Down Expand Up @@ -136,6 +138,27 @@ def test_get_detected_agents(tmp_path):
assert detected[0].display_name == 'Kiro'


def test_get_detected_real_agents_excludes_universal_row(tmp_path):
# Only the universal row detects (``~/.agents`` exists) but no real
# per-agent directory does. The wizard would find nothing here, so the
# hint must not treat this as an eligible detection.
(tmp_path / '.agents').mkdir()
test_configs = [
AgentConfig(
id='cursor',
display_name='Cursor',
detection_path=str(tmp_path / '.cursor'),
),
AgentConfig(
id=UNIVERSAL_ROW_ID,
display_name='Universal',
detection_path=str(tmp_path / '.agents'),
),
]
assert get_detected_agents(agent_configs=test_configs)
assert get_detected_real_agents(agent_configs=test_configs) == []


def test_mcp_config_path_honors_detection_env_override(tmp_path, monkeypatch):
(tmp_path / '.test-agent').mkdir()
override_dir = tmp_path / '.custom-location'
Expand Down
Loading
Loading