From 751073e4b42c7a7b85387502f85d3494276f9ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A1=82=E9=A9=AC?= Date: Mon, 1 Jun 2026 17:49:18 +0800 Subject: [PATCH] fix: consolidate issue batch fixes - restore Shift+Enter newline and Ctrl+R history search behavior - support iac aliyun template variant spelling - route new user-facing validation errors through i18n catalogs --- .gitignore | 1 + pyproject.toml | 4 +- src/iac_code/acp/slash_registry.py | 2 +- src/iac_code/agent/agent_loop.py | 47 +++ src/iac_code/cli/headless.py | 54 +++ src/iac_code/cli/main.py | 29 +- src/iac_code/cli/output_formats.py | 7 +- src/iac_code/commands/clear.py | 2 +- src/iac_code/config.py | 37 +- .../i18n/locales/de/LC_MESSAGES/messages.po | 344 +++++++++++------- .../i18n/locales/es/LC_MESSAGES/messages.po | 344 +++++++++++------- .../i18n/locales/fr/LC_MESSAGES/messages.po | 344 +++++++++++------- .../i18n/locales/ja/LC_MESSAGES/messages.po | 342 ++++++++++------- .../i18n/locales/pt/LC_MESSAGES/messages.po | 344 +++++++++++------- .../i18n/locales/zh/LC_MESSAGES/messages.po | 342 ++++++++++------- src/iac_code/memory/memory_manager.py | 67 +++- src/iac_code/services/agent_factory.py | 1 + src/iac_code/services/permissions/loader.py | 16 +- src/iac_code/services/session_storage.py | 12 +- src/iac_code/services/telemetry/fallback.py | 6 +- src/iac_code/skills/auto_trigger.py | 115 ++++++ src/iac_code/skills/bundled/__init__.py | 2 + .../skills/bundled/iac_aliyun/SKILL.md | 6 +- .../skills/bundled/iac_aliyun/__init__.py | 8 +- .../skills/bundled/iac_aliyun/auto_trigger.py | 88 +++++ src/iac_code/skills/frontmatter.py | 4 + src/iac_code/skills/skill_definition.py | 4 + src/iac_code/tools/result_storage.py | 35 +- src/iac_code/ui/core/input_history.py | 42 ++- src/iac_code/ui/core/prompt_input.py | 25 +- src/iac_code/ui/core/raw_input.py | 58 +++ src/iac_code/ui/core/raw_input_win.py | 51 +++ src/iac_code/ui/repl.py | 118 +++++- src/iac_code/utils/file_security.py | 14 + src/iac_code/utils/image/store.py | 6 +- src/iac_code/utils/log.py | 10 +- tests/acp/test_slash_registry.py | 6 +- tests/agent/test_agent_loop_new.py | 305 +++++++++++++++- tests/cli/test_headless.py | 283 ++++++++++++++ tests/cli/test_output_formats.py | 15 + tests/commands/test_clear.py | 12 + tests/memory/test_memory_manager.py | 53 +++ tests/memory/test_memory_tools.py | 19 + tests/services/permissions/test_loader.py | 25 ++ tests/services/test_session_storage.py | 9 + tests/skills/bundled/test_iac_skill.py | 6 + tests/skills/test_auto_trigger.py | 219 +++++++++++ tests/skills/test_bundled.py | 14 + tests/skills/test_frontmatter.py | 9 + tests/skills/test_listing.py | 12 + tests/test_config.py | 15 + tests/test_config_dir_env.py | 13 + tests/test_config_env_overrides.py | 47 +++ .../test_telemetry/test_fallback.py | 9 + tests/tools/test_result_storage.py | 35 ++ tests/ui/core/test_input_history.py | 106 ++++++ tests/ui/core/test_prompt_input.py | 59 +++ tests/ui/core/test_raw_input.py | 34 ++ tests/ui/core/test_raw_input_win.py | 72 ++++ tests/ui/test_repl_integration.py | 104 +++++- tests/ui/test_repl_shell_escape.py | 217 +++++++++++ tests/utils/image/test_store.py | 16 + tests/utils/test_file_security.py | 26 ++ tests/utils/test_log_symlink.py | 29 ++ uv.lock | 4 +- website/docs/cli/interactive-mode.md | 23 ++ .../current/cli/interactive-mode.md | 17 + .../current/cli/interactive-mode.md | 17 + .../current/cli/interactive-mode.md | 17 + .../current/cli/interactive-mode.md | 17 + .../current/cli/interactive-mode.md | 17 + .../current/cli/interactive-mode.md | 17 + 72 files changed, 3923 insertions(+), 906 deletions(-) create mode 100644 src/iac_code/skills/auto_trigger.py create mode 100644 src/iac_code/skills/bundled/iac_aliyun/auto_trigger.py create mode 100644 tests/skills/test_auto_trigger.py create mode 100644 tests/ui/test_repl_shell_escape.py diff --git a/.gitignore b/.gitignore index 4922d05..0b7b890 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,4 @@ cython_debug/ # Super Powers .superpowers/ +.worktrees/ diff --git a/pyproject.toml b/pyproject.toml index c7e0c4d..1173f8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,8 +33,8 @@ dependencies = [ "pillow==12.2.0", "cryptography>=42.0", "keyring>=25.0", - "tree-sitter>=0.23", - "tree-sitter-bash>=0.23", + "tree-sitter>=0.25,<0.26", + "tree-sitter-bash>=0.25,<0.26", ] [project.optional-dependencies] diff --git a/src/iac_code/acp/slash_registry.py b/src/iac_code/acp/slash_registry.py index 704b282..24ca565 100644 --- a/src/iac_code/acp/slash_registry.py +++ b/src/iac_code/acp/slash_registry.py @@ -91,7 +91,7 @@ async def _handle_compact(self, agent_loop) -> str: async def _handle_clear(self, agent_loop) -> str: """Clear the agent_loop conversation history.""" try: - agent_loop.context_manager.reset() + agent_loop.reset() except Exception as exc: logger.warning("ACP /clear failed: %s", exc) return _("Clear failed: {error}").format(error=exc) diff --git a/src/iac_code/agent/agent_loop.py b/src/iac_code/agent/agent_loop.py index 6890cb1..bf4deb1 100644 --- a/src/iac_code/agent/agent_loop.py +++ b/src/iac_code/agent/agent_loop.py @@ -32,6 +32,7 @@ ToolResultEvent, ToolUseEndEvent, ToolUseStartEvent, + Usage, ) @@ -69,6 +70,7 @@ def __init__( cwd: str | None = None, permission_context: Any = None, # ToolPermissionContext permission_context_getter: Any = None, # Callable[[], ToolPermissionContext | None] + auto_trigger_skills: list[Any] | None = None, ) -> None: self._provider_manager = provider_manager self.system_prompt = system_prompt @@ -79,6 +81,8 @@ def __init__( self._cwd = cwd or os.getcwd() self._permission_context = permission_context self._permission_context_getter = permission_context_getter + self._auto_trigger_skills = auto_trigger_skills or [] + self._auto_loaded_skills: set[str] = set() self._current_git_branch: str | None = None model_name = "" @@ -224,6 +228,7 @@ async def run_streaming(self, user_input: str | list[ContentBlock]) -> AsyncGene # between turns (user runs git checkout via Bash tool), but # is treated as stable within a single in-flight request. self._refresh_git_branch() + await self._apply_auto_triggers(user_input) self.context_manager.add_user_message(user_input) if self._session_storage: from iac_code.agent.message import Message @@ -535,6 +540,46 @@ async def poll_event_queues(): self.context_manager.add_raw_message(msg) if result.context_modifier is not None: self._apply_context_modifier(result.context_modifier) + else: + yield MessageEndEvent(stop_reason="max_turns", usage=Usage()) + + async def _apply_auto_triggers(self, user_input: str | list[ContentBlock]) -> None: + if not self._auto_trigger_skills: + return + if all(command.name in self._auto_loaded_skills for command in self._auto_trigger_skills): + return + prompt_text = self._auto_trigger_text(user_input) + if not prompt_text: + return + + from iac_code.skills.auto_trigger import process_auto_triggered_skills + + results = await process_auto_triggered_skills( + prompt_text, + self._auto_trigger_skills, + loaded_skill_names=self._auto_loaded_skills, + context_messages=self.context_manager.get_messages(), + session_id=self._session_id, + ) + for result in results: + for msg in result.new_messages: + injected = self.context_manager.add_raw_message(msg) + if self._session_storage: + self._session_storage.append( + self._cwd, + self._session_id, + injected, + git_branch=self._current_git_branch, + ) + if result.context_modifier is not None: + self._apply_context_modifier(result.context_modifier) + + @staticmethod + def _auto_trigger_text(user_input: str | list[ContentBlock]) -> str: + if isinstance(user_input, str): + return user_input + parts = [block.text for block in user_input if isinstance(block, TextBlock)] + return " ".join(part for part in parts if part).strip() def _apply_context_modifier(self, modifier: Any) -> None: """Apply a context modifier from a ToolResult to the current execution context.""" @@ -642,6 +687,7 @@ def replace_session(self, session_id: str, resume_messages: list | None) -> None self._session_id = session_id self._current_git_branch = None + self._auto_loaded_skills.clear() self.context_manager.reset() if resume_messages: self.context_manager.load_messages(resume_messages) @@ -663,6 +709,7 @@ def _refresh_git_branch(self) -> None: self._current_git_branch = None def reset(self) -> None: + self._auto_loaded_skills.clear() self.context_manager.reset() def get_context_usage(self) -> dict: diff --git a/src/iac_code/cli/headless.py b/src/iac_code/cli/headless.py index 70cb5be..a9062a5 100644 --- a/src/iac_code/cli/headless.py +++ b/src/iac_code/cli/headless.py @@ -24,6 +24,11 @@ ErrorEvent, MessageEndEvent, PermissionRequestEvent, + StackInstancesProgressEvent, + StackProgressEvent, + SubAgentToolEvent, + ToolResultEvent, + ToolUseStartEvent, ) from iac_code.utils.background_housekeeping import start_background_housekeeping @@ -33,6 +38,47 @@ __all__ = ["HeadlessRunner", "logger"] +class _ProgressWriter: + """Write human-readable headless progress to stderr.""" + + def __init__(self, stream: IO[str]) -> None: + self._stream = stream + + def handle(self, event: Any) -> None: + line: str | None = None + if isinstance(event, ToolUseStartEvent): + line = _("Tool started: {}").format(event.name) + elif isinstance(event, ToolResultEvent): + if event.is_error: + line = _("Tool failed: {}").format(event.tool_name) + else: + line = _("Tool finished: {}").format(event.tool_name) + elif isinstance(event, SubAgentToolEvent): + if event.is_done: + if event.is_error: + line = _("Child tool failed: {}").format(event.child_tool_name) + else: + line = _("Child tool finished: {}").format(event.child_tool_name) + else: + line = _("Child tool started: {}").format(event.child_tool_name) + elif isinstance(event, StackProgressEvent): + line = _("Stack {}: {} ({:.1f}%)").format( + event.stack_name, + event.status, + event.progress_percentage, + ) + elif isinstance(event, StackInstancesProgressEvent): + line = _("Stack group {}: {} ({}%)").format( + event.stack_group_name, + event.status, + event.progress_percentage, + ) + + if line is not None: + self._stream.write(line + "\n") + self._stream.flush() + + class HeadlessRunner: """Run a single prompt headlessly, auto-approving all permission requests.""" @@ -45,6 +91,8 @@ def __init__( cli_allowed_tools: list[str] | None = None, cli_disallowed_tools: list[str] | None = None, cli_permission_mode: str | None = None, + verbose: bool = False, + progress_stream: IO[str] | None = None, ) -> None: self._model = model self._output_format = output_format @@ -53,6 +101,8 @@ def __init__( self._cli_allowed_tools = cli_allowed_tools self._cli_disallowed_tools = cli_disallowed_tools self._cli_permission_mode = cli_permission_mode + self._verbose = verbose + self._progress_stream = progress_stream or sys.stderr def _print_provider_not_configured(self, exc: Exception) -> None: logger.error("Provider not configured: {}", exc) @@ -91,6 +141,7 @@ async def run(self, prompt: str) -> int: agent_loop = self._create_agent_loop() writer = create_writer(self._output_format, self._output_stream) + progress_writer = _ProgressWriter(self._progress_stream) if self._verbose else None has_error = False hit_max_turns = False @@ -118,6 +169,9 @@ async def run(self, prompt: str) -> int: if isinstance(event, MessageEndEvent) and event.stop_reason == "max_turns": hit_max_turns = True + if progress_writer is not None: + progress_writer.handle(event) + writer.handle(event) except ProviderNotConfiguredError as exc: self._print_provider_not_configured(exc) diff --git a/src/iac_code/cli/main.py b/src/iac_code/cli/main.py index 236393b..060b8c1 100644 --- a/src/iac_code/cli/main.py +++ b/src/iac_code/cli/main.py @@ -84,6 +84,7 @@ def main( output_format: str = typer.Option("text", "--output-format", help=_("Output format: text, json, stream-json")), max_turns: int = typer.Option(100, "--max-turns", help=_("Maximum agent turns in headless mode")), debug: bool = typer.Option(False, "--debug", "-d", help=_("Enable debug logging")), + verbose: bool = typer.Option(False, "--verbose", help=_("Show headless progress on stderr")), version: bool = typer.Option(False, "--version", "-v", "-V", is_eager=True, help=_("Show version and exit")), resume: str = typer.Option("", "--resume", "-r", help=_("Resume a session by ID")), continue_session: bool = typer.Option(False, "--continue", "-c", help=_("Resume the most recent session")), @@ -150,6 +151,18 @@ def main( typer.echo(_("Error: --resume and --continue cannot be used together."), err=True) raise typer.Exit(1) + fmt = None + if prompt: + from iac_code.cli.output_formats import OutputFormat + + normalized_output_format = (output_format or "text").strip().lower() + try: + fmt = OutputFormat(normalized_output_format) + except ValueError as exc: + valid = ", ".join(item.value for item in OutputFormat) + typer.echo(_("Invalid --output-format '{}'. Valid values: {}").format(output_format, valid), err=True) + raise typer.Exit(1) from exc + # Priority: CLI parameter > saved config > default if not model: try: @@ -159,10 +172,21 @@ def main( raise typer.Exit(1) if prompt: + assert fmt is not None + # Read from stdin if prompt is "-" if prompt == "-": prompt = sys.stdin.read().strip() + if permission_mode: + from iac_code.services.permissions.loader import parse_cli_permission_mode + + try: + parse_cli_permission_mode(permission_mode) + except ValueError as exc: + typer.echo(str(exc), err=True) + raise typer.Exit(1) from exc + # Headless mode: generate session_id for logging only session_id = str(uuid.uuid4()) setup_logging(session_id=session_id, debug=debug) @@ -175,7 +199,7 @@ def main( Events.SESSION_STARTED, { "headless": True, - "output_format": output_format or "text", + "output_format": fmt.value, }, ) add_metric(Metrics.SESSION_COUNT, 1, {}) @@ -215,9 +239,7 @@ async def _run_with_handler(coro): return await coro from iac_code.cli.headless import HeadlessRunner - from iac_code.cli.output_formats import OutputFormat - fmt = OutputFormat(output_format) cli_allowed = [s.strip() for s in allowed_tools.split(",") if s.strip()] if allowed_tools else None cli_disallowed = [s.strip() for s in disallowed_tools.split(",") if s.strip()] if disallowed_tools else None try: @@ -228,6 +250,7 @@ async def _run_with_handler(coro): cli_allowed_tools=cli_allowed, cli_disallowed_tools=cli_disallowed, cli_permission_mode=permission_mode or None, + verbose=verbose, ) exit_code = asyncio.run(_run_with_handler(runner.run(prompt))) except _QwenPawError as exc: diff --git a/src/iac_code/cli/output_formats.py b/src/iac_code/cli/output_formats.py index 68285e8..3fbe19e 100644 --- a/src/iac_code/cli/output_formats.py +++ b/src/iac_code/cli/output_formats.py @@ -81,12 +81,17 @@ def handle(self, event: StreamEvent) -> None: entry["result"] = event.result entry["is_error"] = event.is_error elif isinstance(event, MessageEndEvent): - self._usage = { + usage = { "input_tokens": event.usage.input_tokens, "output_tokens": event.usage.output_tokens, "cache_creation_input_tokens": event.usage.cache_creation_input_tokens, "cache_read_input_tokens": event.usage.cache_read_input_tokens, } + is_empty_synthetic_max_turns = ( + event.stop_reason == "max_turns" and self._usage is not None and not any(usage.values()) + ) + if not is_empty_synthetic_max_turns: + self._usage = usage elif isinstance(event, ErrorEvent): self._error = event.error diff --git a/src/iac_code/commands/clear.py b/src/iac_code/commands/clear.py index b131161..541ef68 100644 --- a/src/iac_code/commands/clear.py +++ b/src/iac_code/commands/clear.py @@ -12,7 +12,7 @@ async def clear_command(context=None, **kwargs) -> str: if context and hasattr(context, "repl"): agent_loop = getattr(context.repl, "_agent_loop", None) if agent_loop: - agent_loop.context_manager.reset() + agent_loop.reset() if hasattr(context.repl, "_command_log"): context.repl._command_log.clear() diff --git a/src/iac_code/config.py b/src/iac_code/config.py index e6d4f2d..82ae209 100644 --- a/src/iac_code/config.py +++ b/src/iac_code/config.py @@ -15,6 +15,9 @@ import yaml +from iac_code.i18n import _ +from iac_code.utils.file_security import ensure_private_dir, ensure_private_file + # Default LLM model used when no model is saved in settings DEFAULT_MODEL = "qwen3.7-max" @@ -47,8 +50,9 @@ def _load_yaml(path: Path) -> dict[str, Any]: def _save_yaml(path: Path, data: dict[str, Any]) -> None: """Write *data* to a YAML file, creating parent directories as needed.""" - path.parent.mkdir(parents=True, exist_ok=True) + ensure_private_dir(path.parent) path.write_text(yaml.dump(data, default_flow_style=False, allow_unicode=True), encoding="utf-8") + ensure_private_file(path) # --------------------------------------------------------------------------- @@ -56,15 +60,29 @@ def _save_yaml(path: Path, data: dict[str, Any]) -> None: # --------------------------------------------------------------------------- +def _normalize_provider_lookup_name(value: str) -> str: + """Normalize provider display names and keys for user-facing lookup.""" + return value.lower().replace(" ", "").replace("-", "").replace("_", "") + + def _build_provider_name_to_key() -> dict[str, str]: from iac_code.providers.registry import PROVIDER_REGISTRY mapping: dict[str, str] = {} + + def _add_alias(alias: str, provider_key: str) -> None: + normalized = _normalize_provider_lookup_name(alias) + existing = mapping.get(normalized) + if existing is not None and existing != provider_key: + raise ValueError( + f"Ambiguous provider alias {alias!r}: normalized form {normalized!r} " + f"maps to both {existing!r} and {provider_key!r}" + ) + mapping[normalized] = provider_key + for desc in PROVIDER_REGISTRY.values(): - normalized = desc.name.lower().replace(" ", "").replace("-", "").replace("_", "") - mapping[normalized] = desc.key - key_norm = desc.key.lower().replace("_", "").replace("-", "") - mapping[key_norm] = desc.key + _add_alias(desc.name, desc.key) + _add_alias(desc.key, desc.key) return mapping @@ -139,11 +157,13 @@ def _read(name: str) -> str | None: provider_raw = _read("IAC_CODE_PROVIDER") provider_key: str | None = None if provider_raw is not None: - key = _PROVIDER_NAME_TO_KEY.get(provider_raw.lower()) + key = _PROVIDER_NAME_TO_KEY.get(_normalize_provider_lookup_name(provider_raw)) if key is None: valid = ", ".join(_PROVIDER_CANONICAL_NAMES) raise ValueError( - f"Invalid IAC_CODE_PROVIDER value: {provider_raw!r}. Valid values (case-insensitive): {valid}" + _("Invalid IAC_CODE_PROVIDER value: {!r}. Valid values (case-insensitive): {}").format( + provider_raw, valid + ) ) provider_key = key @@ -248,8 +268,7 @@ def get_config_dir() -> Path: caching). """ config_dir = _resolve_config_dir() - config_dir.mkdir(parents=True, exist_ok=True) - return config_dir + return ensure_private_dir(config_dir) def get_credentials_path() -> Path: diff --git a/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po index 1ef6b61..ae5fdbb 100644 --- a/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.3.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-01 16:49+0800\n" +"POT-Creation-Date: 2026-06-01 17:56+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: de\n" @@ -17,6 +17,13 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" +#: src/iac_code/config.py:164 +#, python-brace-format +msgid "Invalid IAC_CODE_PROVIDER value: {!r}. Valid values (case-insensitive): {}" +msgstr "" +"Ungültiger IAC_CODE_PROVIDER-Wert: {!r}. Gültige Werte " +"(Groß-/Kleinschreibung wird ignoriert): {}" + #: src/iac_code/a2a/transports/base.py:175 msgid "" "Unix domain socket transport is not supported on Windows. Use --transport" @@ -101,7 +108,8 @@ msgstr "Debug-Protokollierung deaktiviert." msgid "Usage: /debug [on|off]" msgstr "Verwendung: /debug [on|off]" -#: src/iac_code/agent/agent_loop.py:399 src/iac_code/agent/agent_loop.py:414 +#: src/iac_code/agent/agent_loop.py:404 src/iac_code/agent/agent_loop.py:419 +#: src/iac_code/ui/repl.py:757 src/iac_code/ui/repl.py:771 msgid "Permission denied." msgstr "Zugriff verweigert." @@ -123,7 +131,47 @@ msgstr "Plan" msgid "Agent" msgstr "Agent" -#: src/iac_code/cli/headless.py:60 +#: src/iac_code/cli/headless.py:50 +#, python-brace-format +msgid "Tool started: {}" +msgstr "Tool gestartet: {}" + +#: src/iac_code/cli/headless.py:53 +#, python-brace-format +msgid "Tool failed: {}" +msgstr "Tool fehlgeschlagen: {}" + +#: src/iac_code/cli/headless.py:55 +#, python-brace-format +msgid "Tool finished: {}" +msgstr "Tool abgeschlossen: {}" + +#: src/iac_code/cli/headless.py:59 +#, python-brace-format +msgid "Child tool failed: {}" +msgstr "Untertool fehlgeschlagen: {}" + +#: src/iac_code/cli/headless.py:61 +#, python-brace-format +msgid "Child tool finished: {}" +msgstr "Untertool abgeschlossen: {}" + +#: src/iac_code/cli/headless.py:63 +#, python-brace-format +msgid "Child tool started: {}" +msgstr "Untertool gestartet: {}" + +#: src/iac_code/cli/headless.py:65 +#, python-brace-format +msgid "Stack {}: {} ({:.1f}%)" +msgstr "Stack {}: {} ({:.1f}%)" + +#: src/iac_code/cli/headless.py:71 +#, python-brace-format +msgid "Stack group {}: {} ({}%)" +msgstr "Stack-Gruppe {}: {} ({}%)" + +#: src/iac_code/cli/headless.py:110 #, python-brace-format msgid "" "\n" @@ -193,8 +241,8 @@ msgstr "Git for Windows über den npmmirror-Spiegel installieren (nur Windows)." msgid "YAML config file containing A2A client options" msgstr "YAML-Konfigurationsdatei mit A2A-Client-Optionen" -#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1478 -#: src/iac_code/cli/main.py:1839 src/iac_code/cli/main.py:1878 +#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1501 +#: src/iac_code/cli/main.py:1862 src/iac_code/cli/main.py:1901 msgid "" "A2A client dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -218,28 +266,32 @@ msgstr "Ausgabeformat: text, json, stream-json" msgid "Maximum agent turns in headless mode" msgstr "Maximale Agent-Runden im Headless-Modus" -#: src/iac_code/cli/main.py:86 src/iac_code/cli/main.py:343 -#: src/iac_code/cli/main.py:568 +#: src/iac_code/cli/main.py:86 src/iac_code/cli/main.py:366 +#: src/iac_code/cli/main.py:591 msgid "Enable debug logging" msgstr "Debug-Protokollierung aktivieren" #: src/iac_code/cli/main.py:87 +msgid "Show headless progress on stderr" +msgstr "Headless-Fortschritt auf stderr anzeigen" + +#: src/iac_code/cli/main.py:88 msgid "Show version and exit" msgstr "Version anzeigen und beenden" -#: src/iac_code/cli/main.py:88 +#: src/iac_code/cli/main.py:89 msgid "Resume a session by ID" msgstr "Eine Sitzung anhand der ID fortsetzen" -#: src/iac_code/cli/main.py:89 +#: src/iac_code/cli/main.py:90 msgid "Resume the most recent session" msgstr "Die zuletzt genutzte Sitzung fortsetzen" -#: src/iac_code/cli/main.py:96 src/iac_code/i18n/__init__.py:55 +#: src/iac_code/cli/main.py:97 src/iac_code/i18n/__init__.py:55 msgid "Install completion for the current shell." msgstr "Vervollständigung für die aktuelle Shell installieren." -#: src/iac_code/cli/main.py:104 src/iac_code/i18n/__init__.py:56 +#: src/iac_code/cli/main.py:105 src/iac_code/i18n/__init__.py:56 msgid "" "Show completion for the current shell, to copy it or customize the " "installation." @@ -247,7 +299,7 @@ msgstr "" "Vervollständigung für die aktuelle Shell anzeigen, zum Kopieren oder " "Anpassen der Installation." -#: src/iac_code/cli/main.py:109 +#: src/iac_code/cli/main.py:110 msgid "" "Comma-separated tool permission patterns to allow, e.g. 'bash(git " "*),write_file'" @@ -255,48 +307,53 @@ msgstr "" "Durch Komma getrennte Tool-Berechtigungsmuster zum Erlauben, z.B. " "'bash(git *),write_file'*),write_file'" -#: src/iac_code/cli/main.py:114 +#: src/iac_code/cli/main.py:115 msgid "Comma-separated tool permission patterns to deny" msgstr "Durch Komma getrennte Tool-Berechtigungsmuster zum Verweigern" -#: src/iac_code/cli/main.py:119 +#: src/iac_code/cli/main.py:120 msgid "Permission mode: default, accept_edits, bypass_permissions, dont_ask" msgstr "" "Permission mode: default, accept_edits, bypass_permissions, " "dont_askBerechtigungsmodus: default, accept_edits, bypass_permissions, " "dont_ask" -#: src/iac_code/cli/main.py:150 +#: src/iac_code/cli/main.py:151 msgid "Error: --resume and --continue cannot be used together." msgstr "" "Error: --resume and --continue cannot be used together.Fehler: --resume " "und --continue können nicht gemeinsam verwendet werden." -#: src/iac_code/cli/main.py:338 +#: src/iac_code/cli/main.py:163 +#, python-brace-format +msgid "Invalid --output-format '{}'. Valid values: {}" +msgstr "Ungültiges --output-format '{}'. Gültige Werte: {}" + +#: src/iac_code/cli/main.py:361 msgid "Run iac-code as an ACP server." msgstr "iac-code als ACP-Server ausführen." -#: src/iac_code/cli/main.py:340 +#: src/iac_code/cli/main.py:363 msgid "Transport type: stdio or http" msgstr "Transporttyp: stdio oder http" -#: src/iac_code/cli/main.py:341 +#: src/iac_code/cli/main.py:364 msgid "HTTP server port" msgstr "HTTP-Server-Port" -#: src/iac_code/cli/main.py:342 src/iac_code/cli/main.py:558 +#: src/iac_code/cli/main.py:365 src/iac_code/cli/main.py:581 msgid "HTTP server host" msgstr "HTTP-Server-Host" -#: src/iac_code/cli/main.py:554 +#: src/iac_code/cli/main.py:577 msgid "Run iac-code as an A2A 1.0 server." msgstr "iac-code als A2A 1.0-Server ausführen." -#: src/iac_code/cli/main.py:557 +#: src/iac_code/cli/main.py:580 msgid "YAML config file for A2A server options" msgstr "YAML-Konfigurationsdatei für A2A-Server-Optionen" -#: src/iac_code/cli/main.py:561 +#: src/iac_code/cli/main.py:584 msgid "" "HTTP server port. 41242 is the iac-code default inspired by Gemini CLI, " "not a registered A2A port." @@ -304,7 +361,7 @@ msgstr "" "HTTP-Serverport. 41242 ist der von Gemini CLI inspirierte iac-code-" "Standard, kein registrierter A2A-Port." -#: src/iac_code/cli/main.py:566 +#: src/iac_code/cli/main.py:589 msgid "" "A2A transport: http, stdio, unix, websocket, grpc, grpc-jsonrpc, or " "redis-streams" @@ -312,7 +369,7 @@ msgstr "" "A2A-Transport: http, stdio, unix, websocket, grpc, grpc-jsonrpc oder " "redis-streams" -#: src/iac_code/cli/main.py:572 +#: src/iac_code/cli/main.py:595 msgid "" "Expose A2A thinking signal types; repeat for multiple. Values: raw-" "thinking, tool-trace." @@ -320,7 +377,7 @@ msgstr "" "Legt A2A-Thinking-Signaltypen offen; fuer mehrere Werte wiederholen. " "Werte: raw-thinking, tool-trace." -#: src/iac_code/cli/main.py:623 +#: src/iac_code/cli/main.py:646 msgid "" "A2A server dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -328,228 +385,228 @@ msgstr "" "A2A-Server-Abhängigkeiten fehlen. Installieren mit: pip install 'iac-" "code[a2a]'" -#: src/iac_code/cli/main.py:755 +#: src/iac_code/cli/main.py:778 msgid "Send a prompt to an A2A JSON-RPC endpoint." msgstr "Sendet einen Prompt an einen A2A-JSON-RPC-Endpunkt." -#: src/iac_code/cli/main.py:758 src/iac_code/cli/main.py:928 -#: src/iac_code/cli/main.py:981 src/iac_code/cli/main.py:1041 -#: src/iac_code/cli/main.py:1091 src/iac_code/cli/main.py:1140 -#: src/iac_code/cli/main.py:1219 src/iac_code/cli/main.py:1276 -#: src/iac_code/cli/main.py:1332 src/iac_code/cli/main.py:1389 +#: src/iac_code/cli/main.py:781 src/iac_code/cli/main.py:951 +#: src/iac_code/cli/main.py:1004 src/iac_code/cli/main.py:1064 +#: src/iac_code/cli/main.py:1114 src/iac_code/cli/main.py:1163 +#: src/iac_code/cli/main.py:1242 src/iac_code/cli/main.py:1299 +#: src/iac_code/cli/main.py:1355 src/iac_code/cli/main.py:1412 msgid "A2A JSON-RPC endpoint URL" msgstr "URL des A2A-JSON-RPC-Endpunkts" -#: src/iac_code/cli/main.py:759 src/iac_code/cli/main.py:1434 +#: src/iac_code/cli/main.py:782 src/iac_code/cli/main.py:1457 msgid "Route spec: name=url;skills=skill1,skill2;tags=tag1,tag2" msgstr "Routen-Spezifikation: name=url;skills=skill1,skill2;tags=tag1,tag2" -#: src/iac_code/cli/main.py:760 +#: src/iac_code/cli/main.py:783 msgid "Named A2A route to call" msgstr "Aufzurufende benannte A2A-Route" -#: src/iac_code/cli/main.py:761 +#: src/iac_code/cli/main.py:784 msgid "Prompt to send" msgstr "Zu sendender Prompt" -#: src/iac_code/cli/main.py:762 +#: src/iac_code/cli/main.py:785 msgid "Working directory metadata to send with the request" msgstr "Arbeitsverzeichnis-Metadaten, die mit der Anfrage gesendet werden" -#: src/iac_code/cli/main.py:763 +#: src/iac_code/cli/main.py:786 msgid "A2A context ID to continue" msgstr "Fortzusetzende A2A-Kontext-ID" -#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:859 -#: src/iac_code/cli/main.py:931 src/iac_code/cli/main.py:988 -#: src/iac_code/cli/main.py:1043 src/iac_code/cli/main.py:1093 -#: src/iac_code/cli/main.py:1147 src/iac_code/cli/main.py:1222 -#: src/iac_code/cli/main.py:1280 src/iac_code/cli/main.py:1335 -#: src/iac_code/cli/main.py:1390 +#: src/iac_code/cli/main.py:787 src/iac_code/cli/main.py:882 +#: src/iac_code/cli/main.py:954 src/iac_code/cli/main.py:1011 +#: src/iac_code/cli/main.py:1066 src/iac_code/cli/main.py:1116 +#: src/iac_code/cli/main.py:1170 src/iac_code/cli/main.py:1245 +#: src/iac_code/cli/main.py:1303 src/iac_code/cli/main.py:1358 +#: src/iac_code/cli/main.py:1413 msgid "Bearer token for A2A HTTP requests" msgstr "Bearer-Token für A2A-HTTP-Anfragen" -#: src/iac_code/cli/main.py:765 src/iac_code/cli/main.py:860 -#: src/iac_code/cli/main.py:932 src/iac_code/cli/main.py:989 -#: src/iac_code/cli/main.py:1044 src/iac_code/cli/main.py:1094 -#: src/iac_code/cli/main.py:1148 src/iac_code/cli/main.py:1223 -#: src/iac_code/cli/main.py:1281 src/iac_code/cli/main.py:1336 -#: src/iac_code/cli/main.py:1391 +#: src/iac_code/cli/main.py:788 src/iac_code/cli/main.py:883 +#: src/iac_code/cli/main.py:955 src/iac_code/cli/main.py:1012 +#: src/iac_code/cli/main.py:1067 src/iac_code/cli/main.py:1117 +#: src/iac_code/cli/main.py:1171 src/iac_code/cli/main.py:1246 +#: src/iac_code/cli/main.py:1304 src/iac_code/cli/main.py:1359 +#: src/iac_code/cli/main.py:1414 msgid "Basic auth username for A2A HTTP requests" msgstr "Basic-Auth-Benutzername für A2A-HTTP-Anfragen" -#: src/iac_code/cli/main.py:766 src/iac_code/cli/main.py:861 -#: src/iac_code/cli/main.py:933 src/iac_code/cli/main.py:990 -#: src/iac_code/cli/main.py:1045 src/iac_code/cli/main.py:1095 -#: src/iac_code/cli/main.py:1149 src/iac_code/cli/main.py:1224 -#: src/iac_code/cli/main.py:1282 src/iac_code/cli/main.py:1337 -#: src/iac_code/cli/main.py:1392 +#: src/iac_code/cli/main.py:789 src/iac_code/cli/main.py:884 +#: src/iac_code/cli/main.py:956 src/iac_code/cli/main.py:1013 +#: src/iac_code/cli/main.py:1068 src/iac_code/cli/main.py:1118 +#: src/iac_code/cli/main.py:1172 src/iac_code/cli/main.py:1247 +#: src/iac_code/cli/main.py:1305 src/iac_code/cli/main.py:1360 +#: src/iac_code/cli/main.py:1415 msgid "Basic auth password for A2A HTTP requests" msgstr "Basic-Auth-Passwort für A2A-HTTP-Anfragen" -#: src/iac_code/cli/main.py:767 src/iac_code/cli/main.py:862 -#: src/iac_code/cli/main.py:934 src/iac_code/cli/main.py:991 -#: src/iac_code/cli/main.py:1046 src/iac_code/cli/main.py:1096 -#: src/iac_code/cli/main.py:1150 src/iac_code/cli/main.py:1225 -#: src/iac_code/cli/main.py:1283 src/iac_code/cli/main.py:1338 -#: src/iac_code/cli/main.py:1393 +#: src/iac_code/cli/main.py:790 src/iac_code/cli/main.py:885 +#: src/iac_code/cli/main.py:957 src/iac_code/cli/main.py:1014 +#: src/iac_code/cli/main.py:1069 src/iac_code/cli/main.py:1119 +#: src/iac_code/cli/main.py:1173 src/iac_code/cli/main.py:1248 +#: src/iac_code/cli/main.py:1306 src/iac_code/cli/main.py:1361 +#: src/iac_code/cli/main.py:1416 msgid "API key for A2A HTTP requests" msgstr "API-Schlüssel für A2A-HTTP-Anfragen" -#: src/iac_code/cli/main.py:768 src/iac_code/cli/main.py:863 -#: src/iac_code/cli/main.py:935 src/iac_code/cli/main.py:992 -#: src/iac_code/cli/main.py:1047 src/iac_code/cli/main.py:1097 -#: src/iac_code/cli/main.py:1151 src/iac_code/cli/main.py:1226 -#: src/iac_code/cli/main.py:1284 src/iac_code/cli/main.py:1339 -#: src/iac_code/cli/main.py:1394 +#: src/iac_code/cli/main.py:791 src/iac_code/cli/main.py:886 +#: src/iac_code/cli/main.py:958 src/iac_code/cli/main.py:1015 +#: src/iac_code/cli/main.py:1070 src/iac_code/cli/main.py:1120 +#: src/iac_code/cli/main.py:1174 src/iac_code/cli/main.py:1249 +#: src/iac_code/cli/main.py:1307 src/iac_code/cli/main.py:1362 +#: src/iac_code/cli/main.py:1417 msgid "HTTP header name for A2A API key" msgstr "HTTP-Header-Name für den A2A-API-Schlüssel" -#: src/iac_code/cli/main.py:773 src/iac_code/cli/main.py:868 +#: src/iac_code/cli/main.py:796 src/iac_code/cli/main.py:891 msgid "Secret used to verify the A2A Agent Card" msgstr "Geheimnis zur Verifizierung der A2A Agent Card" -#: src/iac_code/cli/main.py:778 src/iac_code/cli/main.py:873 +#: src/iac_code/cli/main.py:801 src/iac_code/cli/main.py:896 msgid "Remote JWKS URL used to verify the A2A Agent Card" msgstr "Remote-JWKS-URL zur Verifizierung der A2A Agent Card" -#: src/iac_code/cli/main.py:784 src/iac_code/cli/main.py:879 +#: src/iac_code/cli/main.py:807 src/iac_code/cli/main.py:902 msgid "Require a valid A2A Agent Card signature" msgstr "Eine gültige A2A-Agent-Card-Signatur erfordern" -#: src/iac_code/cli/main.py:786 +#: src/iac_code/cli/main.py:809 msgid "A2A call timeout in seconds" msgstr "A2A-Aufruf-Timeout in Sekunden" -#: src/iac_code/cli/main.py:787 +#: src/iac_code/cli/main.py:810 msgid "Use A2A streaming message delivery" msgstr "A2A-Streaming-Nachrichtenübertragung verwenden" -#: src/iac_code/cli/main.py:855 +#: src/iac_code/cli/main.py:878 msgid "Discover an A2A Agent Card." msgstr "Eine A2A Agent Card entdecken." -#: src/iac_code/cli/main.py:858 +#: src/iac_code/cli/main.py:881 msgid "A2A agent base URL" msgstr "Basis-URL des A2A-Agenten" -#: src/iac_code/cli/main.py:925 +#: src/iac_code/cli/main.py:948 msgid "Get an A2A task." msgstr "Eine A2A-Aufgabe abrufen." -#: src/iac_code/cli/main.py:929 src/iac_code/cli/main.py:1042 -#: src/iac_code/cli/main.py:1092 src/iac_code/cli/main.py:1141 -#: src/iac_code/cli/main.py:1220 src/iac_code/cli/main.py:1277 -#: src/iac_code/cli/main.py:1333 +#: src/iac_code/cli/main.py:952 src/iac_code/cli/main.py:1065 +#: src/iac_code/cli/main.py:1115 src/iac_code/cli/main.py:1164 +#: src/iac_code/cli/main.py:1243 src/iac_code/cli/main.py:1300 +#: src/iac_code/cli/main.py:1356 msgid "A2A task ID" msgstr "A2A-Aufgaben-ID" -#: src/iac_code/cli/main.py:930 +#: src/iac_code/cli/main.py:953 msgid "Maximum task history items to return" msgstr "Maximale Anzahl zurückzugebender Aufgabenverlaufselemente" -#: src/iac_code/cli/main.py:978 +#: src/iac_code/cli/main.py:1001 msgid "List A2A tasks." msgstr "A2A-Aufgaben auflisten." -#: src/iac_code/cli/main.py:982 +#: src/iac_code/cli/main.py:1005 msgid "Filter by A2A context ID" msgstr "Nach A2A-Kontext-ID filtern" -#: src/iac_code/cli/main.py:983 +#: src/iac_code/cli/main.py:1006 msgid "Filter by A2A task state" msgstr "Nach A2A-Aufgabenzustand filtern" -#: src/iac_code/cli/main.py:984 +#: src/iac_code/cli/main.py:1007 msgid "Maximum tasks to return" msgstr "Maximale Anzahl zurückzugebender Aufgaben" -#: src/iac_code/cli/main.py:985 src/iac_code/cli/main.py:1279 +#: src/iac_code/cli/main.py:1008 src/iac_code/cli/main.py:1302 msgid "Pagination token" msgstr "Paginierungs-Token" -#: src/iac_code/cli/main.py:986 +#: src/iac_code/cli/main.py:1009 msgid "Include task artifacts" msgstr "Aufgaben-Artefakte einschließen" -#: src/iac_code/cli/main.py:987 +#: src/iac_code/cli/main.py:1010 msgid "Output format: table or json" msgstr "Ausgabeformat: table oder json" -#: src/iac_code/cli/main.py:1038 +#: src/iac_code/cli/main.py:1061 msgid "Cancel an A2A task." msgstr "Eine A2A-Aufgabe abbrechen." -#: src/iac_code/cli/main.py:1088 +#: src/iac_code/cli/main.py:1111 msgid "Subscribe to an A2A task event stream." msgstr "A2A-Aufgaben-Ereignisstrom abonnieren." -#: src/iac_code/cli/main.py:1137 +#: src/iac_code/cli/main.py:1160 msgid "Create an A2A task push notification config." msgstr "Eine Push-Benachrichtigungskonfiguration für eine A2A-Aufgabe erstellen." -#: src/iac_code/cli/main.py:1142 src/iac_code/cli/main.py:1221 -#: src/iac_code/cli/main.py:1334 +#: src/iac_code/cli/main.py:1165 src/iac_code/cli/main.py:1244 +#: src/iac_code/cli/main.py:1357 msgid "Push config ID" msgstr "Push-Konfigurations-ID" -#: src/iac_code/cli/main.py:1143 +#: src/iac_code/cli/main.py:1166 msgid "Push callback URL" msgstr "Push-Callback-URL" -#: src/iac_code/cli/main.py:1144 +#: src/iac_code/cli/main.py:1167 msgid "Notification verification token" msgstr "Token zur Benachrichtigungsverifizierung" -#: src/iac_code/cli/main.py:1145 +#: src/iac_code/cli/main.py:1168 msgid "Callback authentication scheme" msgstr "Callback-Authentifizierungsschema" -#: src/iac_code/cli/main.py:1146 +#: src/iac_code/cli/main.py:1169 msgid "Callback authentication credentials" msgstr "Callback-Authentifizierungsanmeldeinformationen" -#: src/iac_code/cli/main.py:1216 +#: src/iac_code/cli/main.py:1239 msgid "Get an A2A task push notification config." msgstr "Eine Push-Benachrichtigungskonfiguration für eine A2A-Aufgabe abrufen." -#: src/iac_code/cli/main.py:1273 +#: src/iac_code/cli/main.py:1296 msgid "List A2A task push notification configs." msgstr "Push-Benachrichtigungskonfigurationen für A2A-Aufgaben auflisten." -#: src/iac_code/cli/main.py:1278 +#: src/iac_code/cli/main.py:1301 msgid "Maximum configs to return" msgstr "Maximale Anzahl zurückzugebender Konfigurationen" -#: src/iac_code/cli/main.py:1329 +#: src/iac_code/cli/main.py:1352 msgid "Delete an A2A task push notification config." msgstr "Eine Push-Benachrichtigungskonfiguration für eine A2A-Aufgabe löschen." -#: src/iac_code/cli/main.py:1386 +#: src/iac_code/cli/main.py:1409 msgid "Get an authenticated extended A2A Agent Card." msgstr "Eine authentifizierte erweiterte A2A Agent Card abrufen." -#: src/iac_code/cli/main.py:1429 +#: src/iac_code/cli/main.py:1452 msgid "Preview A2A route resolution." msgstr "Vorschau der A2A-Routenauflösung." -#: src/iac_code/cli/main.py:1436 +#: src/iac_code/cli/main.py:1459 msgid "Route name to resolve" msgstr "Aufzulösender Routenname" -#: src/iac_code/cli/main.py:1437 +#: src/iac_code/cli/main.py:1460 msgid "Skill ID to resolve" msgstr "Aufzulösende Skill-ID" -#: src/iac_code/cli/main.py:1438 +#: src/iac_code/cli/main.py:1461 msgid "Prompt text used for tag/name route matching" msgstr "Prompt-Text für die Tag-/Namens-Routenübereinstimmung" -#: src/iac_code/cli/main.py:1443 +#: src/iac_code/cli/main.py:1466 msgid "Directory for persisted A2A routes" msgstr "Verzeichnis für persistierte A2A-Routen" -#: src/iac_code/cli/main.py:1445 +#: src/iac_code/cli/main.py:1468 msgid "Save the provided routes as a route snapshot" msgstr "Speichert die angegebenen Routen als Routen-Snapshot" @@ -598,7 +655,7 @@ msgid "[conversation id or search term]" msgstr "[Konversations-ID oder Suchbegriff]" #: src/iac_code/commands/auth.py:382 src/iac_code/commands/auth.py:1107 -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Navigate" msgstr "Navigieren" @@ -606,7 +663,7 @@ msgstr "Navigieren" #: src/iac_code/commands/auth.py:562 src/iac_code/commands/auth.py:569 #: src/iac_code/commands/auth.py:604 src/iac_code/commands/auth.py:611 #: src/iac_code/commands/auth.py:632 src/iac_code/commands/auth.py:1107 -#: src/iac_code/commands/auth.py:1319 src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/commands/auth.py:1319 src/iac_code/ui/core/prompt_input.py:557 msgid "Confirm" msgstr "Bestätigen" @@ -1131,6 +1188,11 @@ msgstr "" "deaktivieren Sie den QwenPaw-Modus (entfernen Sie 'llm_source: qwenpaw' " "aus settings.yml)." +#: src/iac_code/services/permissions/loader.py:50 +#, python-brace-format +msgid "Invalid --permission-mode {!r}. Valid values: {}" +msgstr "Ungültiges --permission-mode {!r}. Gültige Werte: {}" + #: src/iac_code/services/permissions/pipeline.py:54 #: src/iac_code/tools/base.py:199 src/iac_code/tools/bash/bash_tool.py:175 #, python-brace-format @@ -1857,95 +1919,103 @@ msgstr "Nein, immer \"{rule}\" ablehnen (diese Sitzung)" msgid "No, always reject this tool" msgstr "Nein, dieses Tool immer ablehnen" -#: src/iac_code/ui/repl.py:369 +#: src/iac_code/ui/repl.py:370 msgid "Press Ctrl+C again to exit." msgstr "Drücken Sie erneut Ctrl+C zum Beenden." -#: src/iac_code/ui/repl.py:390 +#: src/iac_code/ui/repl.py:395 msgid "Interrupted." msgstr "Unterbrochen." -#: src/iac_code/ui/repl.py:427 +#: src/iac_code/ui/repl.py:432 msgid "Goodbye!" msgstr "Auf Wiedersehen!" -#: src/iac_code/ui/repl.py:428 +#: src/iac_code/ui/repl.py:433 msgid "Resume this session with:" msgstr "Diese Sitzung fortsetzen mit:" -#: src/iac_code/ui/repl.py:450 +#: src/iac_code/ui/repl.py:458 msgid "Update now" msgstr "Jetzt aktualisieren" -#: src/iac_code/ui/repl.py:452 +#: src/iac_code/ui/repl.py:460 msgid "Run the shown update command and exit when it succeeds." msgstr "" "Führt den angezeigten Aktualisierungsbefehl aus und beendet das Programm " "bei Erfolg." -#: src/iac_code/ui/repl.py:455 +#: src/iac_code/ui/repl.py:463 msgid "Skip" msgstr "Überspringen" -#: src/iac_code/ui/repl.py:457 +#: src/iac_code/ui/repl.py:465 msgid "Continue with the current version for this session." msgstr "Für diese Sitzung mit der aktuellen Version fortfahren." -#: src/iac_code/ui/repl.py:460 +#: src/iac_code/ui/repl.py:468 msgid "Skip until next version" msgstr "Bis zur nächsten Version überspringen" -#: src/iac_code/ui/repl.py:462 +#: src/iac_code/ui/repl.py:470 msgid "Hide this update until a newer version is available." msgstr "Dieses Update ausblenden, bis eine neuere Version verfügbar ist." -#: src/iac_code/ui/repl.py:481 src/iac_code/ui/repl.py:493 +#: src/iac_code/ui/repl.py:489 src/iac_code/ui/repl.py:501 msgid "Update command failed. Continuing with the current version." msgstr "" "Der Aktualisierungsbefehl ist fehlgeschlagen. Es wird mit der aktuellen " "Version fortgefahren." -#: src/iac_code/ui/repl.py:486 +#: src/iac_code/ui/repl.py:494 msgid "Update completed. Restart iac-code to continue." msgstr "Update abgeschlossen. Starten Sie iac-code neu, um fortzufahren." -#: src/iac_code/ui/repl.py:524 +#: src/iac_code/ui/repl.py:532 msgid "No image in clipboard." msgstr "Kein Bild in der Zwischenablage." -#: src/iac_code/ui/repl.py:677 +#: src/iac_code/ui/repl.py:718 +msgid "Usage: !" +msgstr "Verwendung: !" + +#: src/iac_code/ui/repl.py:723 +msgid "Shell command support is unavailable." +msgstr "Shell-Befehlsunterstützung ist nicht verfügbar." + +#: src/iac_code/ui/repl.py:787 #, python-brace-format msgid "Unknown skill: ${name}. Type / to list commands and skills." msgstr "Unbekannter Skill: ${name}. Tippe /, um Befehle und Skills aufzulisten." -#: src/iac_code/ui/repl.py:679 +#: src/iac_code/ui/repl.py:789 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "" "Unknown command: /{name}. Type /help for available commands.Unbekannter " "Befehl: /{name}. Geben Sie /help für verfügbare Befehle ein." -#: src/iac_code/ui/repl.py:684 +#: src/iac_code/ui/repl.py:794 #, python-brace-format msgid "$ only invokes skills. Use /{name} instead." msgstr "$ ruft nur Skills auf. Verwende stattdessen /{name}." -#: src/iac_code/ui/repl.py:706 src/iac_code/ui/repl.py:751 +#: src/iac_code/ui/repl.py:816 src/iac_code/ui/repl.py:861 #, python-brace-format msgid "Command error: {error}" msgstr "Befehlsfehler: {error}" -#: src/iac_code/ui/repl.py:713 +#: src/iac_code/ui/repl.py:823 #, python-brace-format msgid "Command has no handler: {name}" msgstr "Kein Handler für Befehl: {name}" -#: src/iac_code/ui/repl.py:1018 +#: src/iac_code/ui/repl.py:1128 #, python-brace-format msgid "Session not found: {session_id}" msgstr "Sitzung nicht gefunden: {session_id}" -#: src/iac_code/ui/repl.py:1037 +#: src/iac_code/ui/repl.py:1147 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1956,19 +2026,19 @@ msgstr "" "Zum Fortsetzen ausführen:\n" " {cmd}" -#: src/iac_code/ui/repl.py:1076 +#: src/iac_code/ui/repl.py:1186 msgid "This conversation is from a different directory." msgstr "Diese Konversation stammt aus einem anderen Verzeichnis." -#: src/iac_code/ui/repl.py:1078 +#: src/iac_code/ui/repl.py:1188 msgid "To resume, run:" msgstr "Zum Fortsetzen ausführen:" -#: src/iac_code/ui/repl.py:1083 +#: src/iac_code/ui/repl.py:1193 msgid "(Command copied to clipboard)" msgstr "(Befehl in die Zwischenablage kopiert)" -#: src/iac_code/ui/repl.py:1240 +#: src/iac_code/ui/repl.py:1350 #, python-brace-format msgid "" "Current model {model} does not support image input. Use /model to switch " @@ -1977,12 +2047,12 @@ msgstr "" "Das aktuelle Modell {model} unterstützt keine Bildeingabe. Verwenden Sie " "/model, um zu einem Vision-fähigen Modell zu wechseln." -#: src/iac_code/ui/repl.py:1249 +#: src/iac_code/ui/repl.py:1359 #, python-brace-format msgid "Image error: {err}" msgstr "Bildfehler: {err}" -#: src/iac_code/ui/repl.py:1266 +#: src/iac_code/ui/repl.py:1376 msgid "" "Failed to persist image to cache; it will only exist in memory for this " "turn." @@ -2022,15 +2092,15 @@ msgstr "Vollständiges Protokoll · ctrl+o zum Umschalten" msgid "No matches found" msgstr "Keine Treffer" -#: src/iac_code/ui/core/prompt_input.py:337 +#: src/iac_code/ui/core/prompt_input.py:348 msgid "Image in clipboard · ctrl+v to paste" msgstr "Bild in der Zwischenablage · Strg+V zum Einfügen" -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Fill" msgstr "Ausfüllen" -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Dismiss" msgstr "Schließen" diff --git a/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po index 23b12f0..8483d4a 100644 --- a/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.3.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-01 16:49+0800\n" +"POT-Creation-Date: 2026-06-01 17:56+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: es\n" @@ -17,6 +17,13 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" +#: src/iac_code/config.py:164 +#, python-brace-format +msgid "Invalid IAC_CODE_PROVIDER value: {!r}. Valid values (case-insensitive): {}" +msgstr "" +"Valor de IAC_CODE_PROVIDER no válido: {!r}. Valores válidos (sin " +"distinguir mayúsculas/minúsculas): {}" + #: src/iac_code/a2a/transports/base.py:175 msgid "" "Unix domain socket transport is not supported on Windows. Use --transport" @@ -104,7 +111,8 @@ msgstr "Registro de depuración deshabilitado." msgid "Usage: /debug [on|off]" msgstr "Uso: /debug [on|off]" -#: src/iac_code/agent/agent_loop.py:399 src/iac_code/agent/agent_loop.py:414 +#: src/iac_code/agent/agent_loop.py:404 src/iac_code/agent/agent_loop.py:419 +#: src/iac_code/ui/repl.py:757 src/iac_code/ui/repl.py:771 msgid "Permission denied." msgstr "Permiso denegado." @@ -128,7 +136,47 @@ msgstr "Plan" msgid "Agent" msgstr "Agente" -#: src/iac_code/cli/headless.py:60 +#: src/iac_code/cli/headless.py:50 +#, python-brace-format +msgid "Tool started: {}" +msgstr "Herramienta iniciada: {}" + +#: src/iac_code/cli/headless.py:53 +#, python-brace-format +msgid "Tool failed: {}" +msgstr "Herramienta falló: {}" + +#: src/iac_code/cli/headless.py:55 +#, python-brace-format +msgid "Tool finished: {}" +msgstr "Herramienta finalizada: {}" + +#: src/iac_code/cli/headless.py:59 +#, python-brace-format +msgid "Child tool failed: {}" +msgstr "Herramienta secundaria falló: {}" + +#: src/iac_code/cli/headless.py:61 +#, python-brace-format +msgid "Child tool finished: {}" +msgstr "Herramienta secundaria finalizada: {}" + +#: src/iac_code/cli/headless.py:63 +#, python-brace-format +msgid "Child tool started: {}" +msgstr "Herramienta secundaria iniciada: {}" + +#: src/iac_code/cli/headless.py:65 +#, python-brace-format +msgid "Stack {}: {} ({:.1f}%)" +msgstr "Pila {}: {} ({:.1f}%)" + +#: src/iac_code/cli/headless.py:71 +#, python-brace-format +msgid "Stack group {}: {} ({}%)" +msgstr "Grupo de pilas {}: {} ({}%)" + +#: src/iac_code/cli/headless.py:110 #, python-brace-format msgid "" "\n" @@ -195,8 +243,8 @@ msgstr "Instalar Git for Windows mediante el espejo npmmirror (solo Windows)." msgid "YAML config file containing A2A client options" msgstr "Archivo de configuración YAML con opciones del cliente A2A" -#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1478 -#: src/iac_code/cli/main.py:1839 src/iac_code/cli/main.py:1878 +#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1501 +#: src/iac_code/cli/main.py:1862 src/iac_code/cli/main.py:1901 msgid "" "A2A client dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -220,28 +268,32 @@ msgstr "Formato de salida: text, json, stream-json" msgid "Maximum agent turns in headless mode" msgstr "Turnos máximos del agente en modo sin interfaz" -#: src/iac_code/cli/main.py:86 src/iac_code/cli/main.py:343 -#: src/iac_code/cli/main.py:568 +#: src/iac_code/cli/main.py:86 src/iac_code/cli/main.py:366 +#: src/iac_code/cli/main.py:591 msgid "Enable debug logging" msgstr "Habilitar el registro de depuración" #: src/iac_code/cli/main.py:87 +msgid "Show headless progress on stderr" +msgstr "Mostrar el progreso headless en stderr" + +#: src/iac_code/cli/main.py:88 msgid "Show version and exit" msgstr "Mostrar la versión y salir" -#: src/iac_code/cli/main.py:88 +#: src/iac_code/cli/main.py:89 msgid "Resume a session by ID" msgstr "Reanudar una sesión por ID" -#: src/iac_code/cli/main.py:89 +#: src/iac_code/cli/main.py:90 msgid "Resume the most recent session" msgstr "Reanudar la sesión más reciente" -#: src/iac_code/cli/main.py:96 src/iac_code/i18n/__init__.py:55 +#: src/iac_code/cli/main.py:97 src/iac_code/i18n/__init__.py:55 msgid "Install completion for the current shell." msgstr "Instalar la finalización automática para el shell actual." -#: src/iac_code/cli/main.py:104 src/iac_code/i18n/__init__.py:56 +#: src/iac_code/cli/main.py:105 src/iac_code/i18n/__init__.py:56 msgid "" "Show completion for the current shell, to copy it or customize the " "installation." @@ -249,7 +301,7 @@ msgstr "" "Mostrar el script de finalización del shell actual para copiarlo o " "personalizar la instalación." -#: src/iac_code/cli/main.py:109 +#: src/iac_code/cli/main.py:110 msgid "" "Comma-separated tool permission patterns to allow, e.g. 'bash(git " "*),write_file'" @@ -257,43 +309,48 @@ msgstr "" "Patrones de permisos de herramientas a permitir (separados por comas), " "p.ej. 'bash(git *),write_file'*),write_file'" -#: src/iac_code/cli/main.py:114 +#: src/iac_code/cli/main.py:115 msgid "Comma-separated tool permission patterns to deny" msgstr "Patrones de permisos de herramientas a denegar (separados por comas)" -#: src/iac_code/cli/main.py:119 +#: src/iac_code/cli/main.py:120 msgid "Permission mode: default, accept_edits, bypass_permissions, dont_ask" msgstr "Modo de permisos: default, accept_edits, bypass_permissions, dont_ask" -#: src/iac_code/cli/main.py:150 +#: src/iac_code/cli/main.py:151 msgid "Error: --resume and --continue cannot be used together." msgstr "Error: --resume y --continue no pueden usarse a la vez." -#: src/iac_code/cli/main.py:338 +#: src/iac_code/cli/main.py:163 +#, python-brace-format +msgid "Invalid --output-format '{}'. Valid values: {}" +msgstr "--output-format '{}' no válido. Valores válidos: {}" + +#: src/iac_code/cli/main.py:361 msgid "Run iac-code as an ACP server." msgstr "Ejecutar iac-code como servidor ACP." -#: src/iac_code/cli/main.py:340 +#: src/iac_code/cli/main.py:363 msgid "Transport type: stdio or http" msgstr "Tipo de transporte: stdio o http" -#: src/iac_code/cli/main.py:341 +#: src/iac_code/cli/main.py:364 msgid "HTTP server port" msgstr "Puerto del servidor HTTP" -#: src/iac_code/cli/main.py:342 src/iac_code/cli/main.py:558 +#: src/iac_code/cli/main.py:365 src/iac_code/cli/main.py:581 msgid "HTTP server host" msgstr "Host del servidor HTTP" -#: src/iac_code/cli/main.py:554 +#: src/iac_code/cli/main.py:577 msgid "Run iac-code as an A2A 1.0 server." msgstr "Ejecuta iac-code como un servidor A2A 1.0." -#: src/iac_code/cli/main.py:557 +#: src/iac_code/cli/main.py:580 msgid "YAML config file for A2A server options" msgstr "Archivo de configuración YAML para opciones del servidor A2A" -#: src/iac_code/cli/main.py:561 +#: src/iac_code/cli/main.py:584 msgid "" "HTTP server port. 41242 is the iac-code default inspired by Gemini CLI, " "not a registered A2A port." @@ -301,7 +358,7 @@ msgstr "" "Puerto del servidor HTTP. 41242 es el valor predeterminado de iac-code " "inspirado en Gemini CLI, no un puerto A2A registrado." -#: src/iac_code/cli/main.py:566 +#: src/iac_code/cli/main.py:589 msgid "" "A2A transport: http, stdio, unix, websocket, grpc, grpc-jsonrpc, or " "redis-streams" @@ -309,7 +366,7 @@ msgstr "" "Transporte A2A: http, stdio, unix, websocket, grpc, grpc-jsonrpc o redis-" "streams" -#: src/iac_code/cli/main.py:572 +#: src/iac_code/cli/main.py:595 msgid "" "Expose A2A thinking signal types; repeat for multiple. Values: raw-" "thinking, tool-trace." @@ -317,7 +374,7 @@ msgstr "" "Expone tipos de señal de thinking A2A; repite para varios. Valores: raw-" "thinking, tool-trace." -#: src/iac_code/cli/main.py:623 +#: src/iac_code/cli/main.py:646 msgid "" "A2A server dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -325,230 +382,230 @@ msgstr "" "Faltan las dependencias del servidor A2A. Instálalas con: pip install " "'iac-code[a2a]'" -#: src/iac_code/cli/main.py:755 +#: src/iac_code/cli/main.py:778 msgid "Send a prompt to an A2A JSON-RPC endpoint." msgstr "Envía un prompt a un endpoint JSON-RPC A2A." -#: src/iac_code/cli/main.py:758 src/iac_code/cli/main.py:928 -#: src/iac_code/cli/main.py:981 src/iac_code/cli/main.py:1041 -#: src/iac_code/cli/main.py:1091 src/iac_code/cli/main.py:1140 -#: src/iac_code/cli/main.py:1219 src/iac_code/cli/main.py:1276 -#: src/iac_code/cli/main.py:1332 src/iac_code/cli/main.py:1389 +#: src/iac_code/cli/main.py:781 src/iac_code/cli/main.py:951 +#: src/iac_code/cli/main.py:1004 src/iac_code/cli/main.py:1064 +#: src/iac_code/cli/main.py:1114 src/iac_code/cli/main.py:1163 +#: src/iac_code/cli/main.py:1242 src/iac_code/cli/main.py:1299 +#: src/iac_code/cli/main.py:1355 src/iac_code/cli/main.py:1412 msgid "A2A JSON-RPC endpoint URL" msgstr "URL del endpoint JSON-RPC A2A" -#: src/iac_code/cli/main.py:759 src/iac_code/cli/main.py:1434 +#: src/iac_code/cli/main.py:782 src/iac_code/cli/main.py:1457 msgid "Route spec: name=url;skills=skill1,skill2;tags=tag1,tag2" msgstr "Especificación de ruta: name=url;skills=skill1,skill2;tags=tag1,tag2" -#: src/iac_code/cli/main.py:760 +#: src/iac_code/cli/main.py:783 msgid "Named A2A route to call" msgstr "Ruta A2A con nombre a llamar" -#: src/iac_code/cli/main.py:761 +#: src/iac_code/cli/main.py:784 msgid "Prompt to send" msgstr "Prompt a enviar" -#: src/iac_code/cli/main.py:762 +#: src/iac_code/cli/main.py:785 msgid "Working directory metadata to send with the request" msgstr "Metadatos del directorio de trabajo a enviar con la solicitud" -#: src/iac_code/cli/main.py:763 +#: src/iac_code/cli/main.py:786 msgid "A2A context ID to continue" msgstr "ID de contexto A2A a continuar" -#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:859 -#: src/iac_code/cli/main.py:931 src/iac_code/cli/main.py:988 -#: src/iac_code/cli/main.py:1043 src/iac_code/cli/main.py:1093 -#: src/iac_code/cli/main.py:1147 src/iac_code/cli/main.py:1222 -#: src/iac_code/cli/main.py:1280 src/iac_code/cli/main.py:1335 -#: src/iac_code/cli/main.py:1390 +#: src/iac_code/cli/main.py:787 src/iac_code/cli/main.py:882 +#: src/iac_code/cli/main.py:954 src/iac_code/cli/main.py:1011 +#: src/iac_code/cli/main.py:1066 src/iac_code/cli/main.py:1116 +#: src/iac_code/cli/main.py:1170 src/iac_code/cli/main.py:1245 +#: src/iac_code/cli/main.py:1303 src/iac_code/cli/main.py:1358 +#: src/iac_code/cli/main.py:1413 msgid "Bearer token for A2A HTTP requests" msgstr "Token Bearer para solicitudes HTTP A2A" -#: src/iac_code/cli/main.py:765 src/iac_code/cli/main.py:860 -#: src/iac_code/cli/main.py:932 src/iac_code/cli/main.py:989 -#: src/iac_code/cli/main.py:1044 src/iac_code/cli/main.py:1094 -#: src/iac_code/cli/main.py:1148 src/iac_code/cli/main.py:1223 -#: src/iac_code/cli/main.py:1281 src/iac_code/cli/main.py:1336 -#: src/iac_code/cli/main.py:1391 +#: src/iac_code/cli/main.py:788 src/iac_code/cli/main.py:883 +#: src/iac_code/cli/main.py:955 src/iac_code/cli/main.py:1012 +#: src/iac_code/cli/main.py:1067 src/iac_code/cli/main.py:1117 +#: src/iac_code/cli/main.py:1171 src/iac_code/cli/main.py:1246 +#: src/iac_code/cli/main.py:1304 src/iac_code/cli/main.py:1359 +#: src/iac_code/cli/main.py:1414 msgid "Basic auth username for A2A HTTP requests" msgstr "Nombre de usuario de autenticación básica para solicitudes HTTP A2A" -#: src/iac_code/cli/main.py:766 src/iac_code/cli/main.py:861 -#: src/iac_code/cli/main.py:933 src/iac_code/cli/main.py:990 -#: src/iac_code/cli/main.py:1045 src/iac_code/cli/main.py:1095 -#: src/iac_code/cli/main.py:1149 src/iac_code/cli/main.py:1224 -#: src/iac_code/cli/main.py:1282 src/iac_code/cli/main.py:1337 -#: src/iac_code/cli/main.py:1392 +#: src/iac_code/cli/main.py:789 src/iac_code/cli/main.py:884 +#: src/iac_code/cli/main.py:956 src/iac_code/cli/main.py:1013 +#: src/iac_code/cli/main.py:1068 src/iac_code/cli/main.py:1118 +#: src/iac_code/cli/main.py:1172 src/iac_code/cli/main.py:1247 +#: src/iac_code/cli/main.py:1305 src/iac_code/cli/main.py:1360 +#: src/iac_code/cli/main.py:1415 msgid "Basic auth password for A2A HTTP requests" msgstr "Contraseña de autenticación básica para solicitudes HTTP A2A" -#: src/iac_code/cli/main.py:767 src/iac_code/cli/main.py:862 -#: src/iac_code/cli/main.py:934 src/iac_code/cli/main.py:991 -#: src/iac_code/cli/main.py:1046 src/iac_code/cli/main.py:1096 -#: src/iac_code/cli/main.py:1150 src/iac_code/cli/main.py:1225 -#: src/iac_code/cli/main.py:1283 src/iac_code/cli/main.py:1338 -#: src/iac_code/cli/main.py:1393 +#: src/iac_code/cli/main.py:790 src/iac_code/cli/main.py:885 +#: src/iac_code/cli/main.py:957 src/iac_code/cli/main.py:1014 +#: src/iac_code/cli/main.py:1069 src/iac_code/cli/main.py:1119 +#: src/iac_code/cli/main.py:1173 src/iac_code/cli/main.py:1248 +#: src/iac_code/cli/main.py:1306 src/iac_code/cli/main.py:1361 +#: src/iac_code/cli/main.py:1416 msgid "API key for A2A HTTP requests" msgstr "Clave de API para solicitudes HTTP A2A" -#: src/iac_code/cli/main.py:768 src/iac_code/cli/main.py:863 -#: src/iac_code/cli/main.py:935 src/iac_code/cli/main.py:992 -#: src/iac_code/cli/main.py:1047 src/iac_code/cli/main.py:1097 -#: src/iac_code/cli/main.py:1151 src/iac_code/cli/main.py:1226 -#: src/iac_code/cli/main.py:1284 src/iac_code/cli/main.py:1339 -#: src/iac_code/cli/main.py:1394 +#: src/iac_code/cli/main.py:791 src/iac_code/cli/main.py:886 +#: src/iac_code/cli/main.py:958 src/iac_code/cli/main.py:1015 +#: src/iac_code/cli/main.py:1070 src/iac_code/cli/main.py:1120 +#: src/iac_code/cli/main.py:1174 src/iac_code/cli/main.py:1249 +#: src/iac_code/cli/main.py:1307 src/iac_code/cli/main.py:1362 +#: src/iac_code/cli/main.py:1417 msgid "HTTP header name for A2A API key" msgstr "Nombre del encabezado HTTP para la clave de API A2A" -#: src/iac_code/cli/main.py:773 src/iac_code/cli/main.py:868 +#: src/iac_code/cli/main.py:796 src/iac_code/cli/main.py:891 msgid "Secret used to verify the A2A Agent Card" msgstr "Secreto utilizado para verificar la A2A Agent Card" -#: src/iac_code/cli/main.py:778 src/iac_code/cli/main.py:873 +#: src/iac_code/cli/main.py:801 src/iac_code/cli/main.py:896 msgid "Remote JWKS URL used to verify the A2A Agent Card" msgstr "URL JWKS remota utilizada para verificar la A2A Agent Card" -#: src/iac_code/cli/main.py:784 src/iac_code/cli/main.py:879 +#: src/iac_code/cli/main.py:807 src/iac_code/cli/main.py:902 msgid "Require a valid A2A Agent Card signature" msgstr "Requerir una firma válida de A2A Agent Card" -#: src/iac_code/cli/main.py:786 +#: src/iac_code/cli/main.py:809 msgid "A2A call timeout in seconds" msgstr "Tiempo de espera de llamada A2A en segundos" -#: src/iac_code/cli/main.py:787 +#: src/iac_code/cli/main.py:810 msgid "Use A2A streaming message delivery" msgstr "Usar entrega de mensajes en streaming A2A" -#: src/iac_code/cli/main.py:855 +#: src/iac_code/cli/main.py:878 msgid "Discover an A2A Agent Card." msgstr "Descubre una A2A Agent Card." -#: src/iac_code/cli/main.py:858 +#: src/iac_code/cli/main.py:881 msgid "A2A agent base URL" msgstr "URL base del agente A2A" -#: src/iac_code/cli/main.py:925 +#: src/iac_code/cli/main.py:948 msgid "Get an A2A task." msgstr "Obtén una tarea A2A." -#: src/iac_code/cli/main.py:929 src/iac_code/cli/main.py:1042 -#: src/iac_code/cli/main.py:1092 src/iac_code/cli/main.py:1141 -#: src/iac_code/cli/main.py:1220 src/iac_code/cli/main.py:1277 -#: src/iac_code/cli/main.py:1333 +#: src/iac_code/cli/main.py:952 src/iac_code/cli/main.py:1065 +#: src/iac_code/cli/main.py:1115 src/iac_code/cli/main.py:1164 +#: src/iac_code/cli/main.py:1243 src/iac_code/cli/main.py:1300 +#: src/iac_code/cli/main.py:1356 msgid "A2A task ID" msgstr "ID de tarea A2A" -#: src/iac_code/cli/main.py:930 +#: src/iac_code/cli/main.py:953 msgid "Maximum task history items to return" msgstr "Número máximo de elementos del historial de tareas a devolver" -#: src/iac_code/cli/main.py:978 +#: src/iac_code/cli/main.py:1001 msgid "List A2A tasks." msgstr "Lista las tareas A2A." -#: src/iac_code/cli/main.py:982 +#: src/iac_code/cli/main.py:1005 msgid "Filter by A2A context ID" msgstr "Filtrar por ID de contexto A2A" -#: src/iac_code/cli/main.py:983 +#: src/iac_code/cli/main.py:1006 msgid "Filter by A2A task state" msgstr "Filtrar por estado de tarea A2A" -#: src/iac_code/cli/main.py:984 +#: src/iac_code/cli/main.py:1007 msgid "Maximum tasks to return" msgstr "Número máximo de tareas a devolver" -#: src/iac_code/cli/main.py:985 src/iac_code/cli/main.py:1279 +#: src/iac_code/cli/main.py:1008 src/iac_code/cli/main.py:1302 msgid "Pagination token" msgstr "Token de paginación" -#: src/iac_code/cli/main.py:986 +#: src/iac_code/cli/main.py:1009 msgid "Include task artifacts" msgstr "Incluir artefactos de tarea" -#: src/iac_code/cli/main.py:987 +#: src/iac_code/cli/main.py:1010 msgid "Output format: table or json" msgstr "Formato de salida: table o json" -#: src/iac_code/cli/main.py:1038 +#: src/iac_code/cli/main.py:1061 msgid "Cancel an A2A task." msgstr "Cancela una tarea A2A." -#: src/iac_code/cli/main.py:1088 +#: src/iac_code/cli/main.py:1111 msgid "Subscribe to an A2A task event stream." msgstr "Suscríbete a un flujo de eventos de tarea A2A." -#: src/iac_code/cli/main.py:1137 +#: src/iac_code/cli/main.py:1160 msgid "Create an A2A task push notification config." msgstr "Crea una configuración de notificación push de tarea A2A." -#: src/iac_code/cli/main.py:1142 src/iac_code/cli/main.py:1221 -#: src/iac_code/cli/main.py:1334 +#: src/iac_code/cli/main.py:1165 src/iac_code/cli/main.py:1244 +#: src/iac_code/cli/main.py:1357 msgid "Push config ID" msgstr "ID de configuración push" -#: src/iac_code/cli/main.py:1143 +#: src/iac_code/cli/main.py:1166 msgid "Push callback URL" msgstr "URL de callback push" -#: src/iac_code/cli/main.py:1144 +#: src/iac_code/cli/main.py:1167 msgid "Notification verification token" msgstr "Token de verificación de notificación" -#: src/iac_code/cli/main.py:1145 +#: src/iac_code/cli/main.py:1168 msgid "Callback authentication scheme" msgstr "Esquema de autenticación de callback" -#: src/iac_code/cli/main.py:1146 +#: src/iac_code/cli/main.py:1169 msgid "Callback authentication credentials" msgstr "Credenciales de autenticación de callback" -#: src/iac_code/cli/main.py:1216 +#: src/iac_code/cli/main.py:1239 msgid "Get an A2A task push notification config." msgstr "Obtén una configuración de notificación push de tarea A2A." -#: src/iac_code/cli/main.py:1273 +#: src/iac_code/cli/main.py:1296 msgid "List A2A task push notification configs." msgstr "Lista las configuraciones de notificación push de tareas A2A." -#: src/iac_code/cli/main.py:1278 +#: src/iac_code/cli/main.py:1301 msgid "Maximum configs to return" msgstr "Número máximo de configuraciones a devolver" -#: src/iac_code/cli/main.py:1329 +#: src/iac_code/cli/main.py:1352 msgid "Delete an A2A task push notification config." msgstr "Elimina una configuración de notificación push de tarea A2A." -#: src/iac_code/cli/main.py:1386 +#: src/iac_code/cli/main.py:1409 msgid "Get an authenticated extended A2A Agent Card." msgstr "Obtén una A2A Agent Card extendida autenticada." -#: src/iac_code/cli/main.py:1429 +#: src/iac_code/cli/main.py:1452 msgid "Preview A2A route resolution." msgstr "Vista previa de la resolución de rutas A2A." -#: src/iac_code/cli/main.py:1436 +#: src/iac_code/cli/main.py:1459 msgid "Route name to resolve" msgstr "Nombre de ruta a resolver" -#: src/iac_code/cli/main.py:1437 +#: src/iac_code/cli/main.py:1460 msgid "Skill ID to resolve" msgstr "ID de habilidad a resolver" -#: src/iac_code/cli/main.py:1438 +#: src/iac_code/cli/main.py:1461 msgid "Prompt text used for tag/name route matching" msgstr "" "Texto del prompt utilizado para la coincidencia de rutas por " "etiqueta/nombre" -#: src/iac_code/cli/main.py:1443 +#: src/iac_code/cli/main.py:1466 msgid "Directory for persisted A2A routes" msgstr "Directorio para rutas A2A persistentes" -#: src/iac_code/cli/main.py:1445 +#: src/iac_code/cli/main.py:1468 msgid "Save the provided routes as a route snapshot" msgstr "Guarda las rutas proporcionadas como una instantánea de rutas" @@ -597,7 +654,7 @@ msgid "[conversation id or search term]" msgstr "[id de conversación o término de búsqueda]" #: src/iac_code/commands/auth.py:382 src/iac_code/commands/auth.py:1107 -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Navigate" msgstr "Navegar" @@ -605,7 +662,7 @@ msgstr "Navegar" #: src/iac_code/commands/auth.py:562 src/iac_code/commands/auth.py:569 #: src/iac_code/commands/auth.py:604 src/iac_code/commands/auth.py:611 #: src/iac_code/commands/auth.py:632 src/iac_code/commands/auth.py:1107 -#: src/iac_code/commands/auth.py:1319 src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/commands/auth.py:1319 src/iac_code/ui/core/prompt_input.py:557 msgid "Confirm" msgstr "Confirmar" @@ -1129,6 +1186,11 @@ msgstr "" "Solución: cambie a un proveedor soportado en QwenPaw, o desactive el modo" " QwenPaw (elimine 'llm_source: qwenpaw' de settings.yml)." +#: src/iac_code/services/permissions/loader.py:50 +#, python-brace-format +msgid "Invalid --permission-mode {!r}. Valid values: {}" +msgstr "Modo --permission-mode no válido: {!r}. Valores válidos: {}" + #: src/iac_code/services/permissions/pipeline.py:54 #: src/iac_code/tools/base.py:199 src/iac_code/tools/bash/bash_tool.py:175 #, python-brace-format @@ -1862,70 +1924,78 @@ msgstr "No, siempre denegar \"{rule}\" (esta sesión)" msgid "No, always reject this tool" msgstr "No, rechazar siempre esta herramienta" -#: src/iac_code/ui/repl.py:369 +#: src/iac_code/ui/repl.py:370 msgid "Press Ctrl+C again to exit." msgstr "Pulse Ctrl+C de nuevo para salir." -#: src/iac_code/ui/repl.py:390 +#: src/iac_code/ui/repl.py:395 msgid "Interrupted." msgstr "Interrumpido." -#: src/iac_code/ui/repl.py:427 +#: src/iac_code/ui/repl.py:432 msgid "Goodbye!" msgstr "¡Hasta luego!" -#: src/iac_code/ui/repl.py:428 +#: src/iac_code/ui/repl.py:433 msgid "Resume this session with:" msgstr "Para reanudar esta sesión, ejecute:" -#: src/iac_code/ui/repl.py:450 +#: src/iac_code/ui/repl.py:458 msgid "Update now" msgstr "Actualizar ahora" -#: src/iac_code/ui/repl.py:452 +#: src/iac_code/ui/repl.py:460 msgid "Run the shown update command and exit when it succeeds." msgstr "" "Ejecuta el comando de actualización mostrado y sale cuando finalice " "correctamente." -#: src/iac_code/ui/repl.py:455 +#: src/iac_code/ui/repl.py:463 msgid "Skip" msgstr "Omitir" -#: src/iac_code/ui/repl.py:457 +#: src/iac_code/ui/repl.py:465 msgid "Continue with the current version for this session." msgstr "Continuar con la versión actual durante esta sesión." -#: src/iac_code/ui/repl.py:460 +#: src/iac_code/ui/repl.py:468 msgid "Skip until next version" msgstr "Omitir hasta la siguiente versión" -#: src/iac_code/ui/repl.py:462 +#: src/iac_code/ui/repl.py:470 msgid "Hide this update until a newer version is available." msgstr "" "Ocultar esta actualización hasta que haya una versión más nueva " "disponible." -#: src/iac_code/ui/repl.py:481 src/iac_code/ui/repl.py:493 +#: src/iac_code/ui/repl.py:489 src/iac_code/ui/repl.py:501 msgid "Update command failed. Continuing with the current version." msgstr "El comando de actualización falló. Se continuará con la versión actual." -#: src/iac_code/ui/repl.py:486 +#: src/iac_code/ui/repl.py:494 msgid "Update completed. Restart iac-code to continue." msgstr "Actualización completada. Reinicia iac-code para continuar." -#: src/iac_code/ui/repl.py:524 +#: src/iac_code/ui/repl.py:532 msgid "No image in clipboard." msgstr "No hay ninguna imagen en el portapapeles." -#: src/iac_code/ui/repl.py:677 +#: src/iac_code/ui/repl.py:718 +msgid "Usage: !" +msgstr "Uso: !" + +#: src/iac_code/ui/repl.py:723 +msgid "Shell command support is unavailable." +msgstr "La compatibilidad con comandos de shell no está disponible." + +#: src/iac_code/ui/repl.py:787 #, python-brace-format msgid "Unknown skill: ${name}. Type / to list commands and skills." msgstr "" "Habilidad desconocida: ${name}. Escribe / para listar comandos y " "habilidades." -#: src/iac_code/ui/repl.py:679 +#: src/iac_code/ui/repl.py:789 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "" @@ -1933,27 +2003,27 @@ msgstr "" "command: /{name}. Type /help for available commands.Comando desconocido: " "/{name}. Escriba /help para ver los comandos disponibles." -#: src/iac_code/ui/repl.py:684 +#: src/iac_code/ui/repl.py:794 #, python-brace-format msgid "$ only invokes skills. Use /{name} instead." msgstr "$ solo invoca habilidades. Usa /{name} en su lugar." -#: src/iac_code/ui/repl.py:706 src/iac_code/ui/repl.py:751 +#: src/iac_code/ui/repl.py:816 src/iac_code/ui/repl.py:861 #, python-brace-format msgid "Command error: {error}" msgstr "Error de comando: {error}" -#: src/iac_code/ui/repl.py:713 +#: src/iac_code/ui/repl.py:823 #, python-brace-format msgid "Command has no handler: {name}" msgstr "El comando no tiene controlador: {name}" -#: src/iac_code/ui/repl.py:1018 +#: src/iac_code/ui/repl.py:1128 #, python-brace-format msgid "Session not found: {session_id}" msgstr "Sesión no encontrada: {session_id}" -#: src/iac_code/ui/repl.py:1037 +#: src/iac_code/ui/repl.py:1147 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1964,19 +2034,19 @@ msgstr "" "Para reanudar, ejecute:\n" " {cmd}" -#: src/iac_code/ui/repl.py:1076 +#: src/iac_code/ui/repl.py:1186 msgid "This conversation is from a different directory." msgstr "Esta conversación procede de otro directorio." -#: src/iac_code/ui/repl.py:1078 +#: src/iac_code/ui/repl.py:1188 msgid "To resume, run:" msgstr "Para reanudar, ejecute:" -#: src/iac_code/ui/repl.py:1083 +#: src/iac_code/ui/repl.py:1193 msgid "(Command copied to clipboard)" msgstr "(Comando copiado al portapapeles)" -#: src/iac_code/ui/repl.py:1240 +#: src/iac_code/ui/repl.py:1350 #, python-brace-format msgid "" "Current model {model} does not support image input. Use /model to switch " @@ -1985,12 +2055,12 @@ msgstr "" "El modelo actual {model} no admite entrada de imágenes. Usa /model para " "cambiar a un modelo con capacidad de visión." -#: src/iac_code/ui/repl.py:1249 +#: src/iac_code/ui/repl.py:1359 #, python-brace-format msgid "Image error: {err}" msgstr "Error de imagen: {err}" -#: src/iac_code/ui/repl.py:1266 +#: src/iac_code/ui/repl.py:1376 msgid "" "Failed to persist image to cache; it will only exist in memory for this " "turn." @@ -2030,15 +2100,15 @@ msgstr "Mostrando transcripción · ctrl+o para alternar" msgid "No matches found" msgstr "No se encontraron coincidencias" -#: src/iac_code/ui/core/prompt_input.py:337 +#: src/iac_code/ui/core/prompt_input.py:348 msgid "Image in clipboard · ctrl+v to paste" msgstr "Imagen en el portapapeles · ctrl+v para pegar" -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Fill" msgstr "Rellenar" -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Dismiss" msgstr "Descartar" diff --git a/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po index e3128fc..cb37d6e 100644 --- a/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.3.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-01 16:49+0800\n" +"POT-Creation-Date: 2026-06-01 17:56+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: fr\n" @@ -17,6 +17,13 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" +#: src/iac_code/config.py:164 +#, python-brace-format +msgid "Invalid IAC_CODE_PROVIDER value: {!r}. Valid values (case-insensitive): {}" +msgstr "" +"Valeur IAC_CODE_PROVIDER invalide : {!r}. Valeurs valides (insensible à " +"la casse) : {}" + #: src/iac_code/a2a/transports/base.py:175 msgid "" "Unix domain socket transport is not supported on Windows. Use --transport" @@ -101,7 +108,8 @@ msgstr "Journalisation debug désactivée." msgid "Usage: /debug [on|off]" msgstr "Utilisation : /debug [on|off]" -#: src/iac_code/agent/agent_loop.py:399 src/iac_code/agent/agent_loop.py:414 +#: src/iac_code/agent/agent_loop.py:404 src/iac_code/agent/agent_loop.py:419 +#: src/iac_code/ui/repl.py:757 src/iac_code/ui/repl.py:771 msgid "Permission denied." msgstr "Permission refusée." @@ -123,7 +131,47 @@ msgstr "Plan" msgid "Agent" msgstr "Agent" -#: src/iac_code/cli/headless.py:60 +#: src/iac_code/cli/headless.py:50 +#, python-brace-format +msgid "Tool started: {}" +msgstr "Outil démarré : {}" + +#: src/iac_code/cli/headless.py:53 +#, python-brace-format +msgid "Tool failed: {}" +msgstr "Outil échoué : {}" + +#: src/iac_code/cli/headless.py:55 +#, python-brace-format +msgid "Tool finished: {}" +msgstr "Outil terminé : {}" + +#: src/iac_code/cli/headless.py:59 +#, python-brace-format +msgid "Child tool failed: {}" +msgstr "Outil enfant échoué : {}" + +#: src/iac_code/cli/headless.py:61 +#, python-brace-format +msgid "Child tool finished: {}" +msgstr "Outil enfant terminé : {}" + +#: src/iac_code/cli/headless.py:63 +#, python-brace-format +msgid "Child tool started: {}" +msgstr "Outil enfant démarré : {}" + +#: src/iac_code/cli/headless.py:65 +#, python-brace-format +msgid "Stack {}: {} ({:.1f}%)" +msgstr "Pile {} : {} ({:.1f}%)" + +#: src/iac_code/cli/headless.py:71 +#, python-brace-format +msgid "Stack group {}: {} ({}%)" +msgstr "Groupe de piles {} : {} ({}%)" + +#: src/iac_code/cli/headless.py:110 #, python-brace-format msgid "" "\n" @@ -193,8 +241,8 @@ msgstr "Installer Git for Windows via le miroir npmmirror (Windows uniquement)." msgid "YAML config file containing A2A client options" msgstr "Fichier de configuration YAML contenant les options client A2A" -#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1478 -#: src/iac_code/cli/main.py:1839 src/iac_code/cli/main.py:1878 +#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1501 +#: src/iac_code/cli/main.py:1862 src/iac_code/cli/main.py:1901 msgid "" "A2A client dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -218,28 +266,32 @@ msgstr "Format de sortie : text, json, stream-json" msgid "Maximum agent turns in headless mode" msgstr "Nombre maximal de tours d’agent en mode headless" -#: src/iac_code/cli/main.py:86 src/iac_code/cli/main.py:343 -#: src/iac_code/cli/main.py:568 +#: src/iac_code/cli/main.py:86 src/iac_code/cli/main.py:366 +#: src/iac_code/cli/main.py:591 msgid "Enable debug logging" msgstr "Activer la journalisation debug" #: src/iac_code/cli/main.py:87 +msgid "Show headless progress on stderr" +msgstr "Afficher la progression headless sur stderr" + +#: src/iac_code/cli/main.py:88 msgid "Show version and exit" msgstr "Afficher la version et quitter" -#: src/iac_code/cli/main.py:88 +#: src/iac_code/cli/main.py:89 msgid "Resume a session by ID" msgstr "Reprendre une session par identifiant" -#: src/iac_code/cli/main.py:89 +#: src/iac_code/cli/main.py:90 msgid "Resume the most recent session" msgstr "Reprendre la session la plus récente" -#: src/iac_code/cli/main.py:96 src/iac_code/i18n/__init__.py:55 +#: src/iac_code/cli/main.py:97 src/iac_code/i18n/__init__.py:55 msgid "Install completion for the current shell." msgstr "Installer la complétion pour le shell actuel." -#: src/iac_code/cli/main.py:104 src/iac_code/i18n/__init__.py:56 +#: src/iac_code/cli/main.py:105 src/iac_code/i18n/__init__.py:56 msgid "" "Show completion for the current shell, to copy it or customize the " "installation." @@ -247,7 +299,7 @@ msgstr "" "Afficher la complétion pour le shell actuel afin de la copier ou de " "personnaliser l’installation." -#: src/iac_code/cli/main.py:109 +#: src/iac_code/cli/main.py:110 msgid "" "Comma-separated tool permission patterns to allow, e.g. 'bash(git " "*),write_file'" @@ -255,45 +307,50 @@ msgstr "" "Modèles de permissions d'outils à autoriser (séparés par des virgules), " "ex. 'bash(git *),write_file'*),write_file'" -#: src/iac_code/cli/main.py:114 +#: src/iac_code/cli/main.py:115 msgid "Comma-separated tool permission patterns to deny" msgstr "Modèles de permissions d'outils à refuser (séparés par des virgules)" -#: src/iac_code/cli/main.py:119 +#: src/iac_code/cli/main.py:120 msgid "Permission mode: default, accept_edits, bypass_permissions, dont_ask" msgstr "" "Permission mode: default, accept_edits, bypass_permissions, dont_askMode " "de permissions : default, accept_edits, bypass_permissions, dont_ask" -#: src/iac_code/cli/main.py:150 +#: src/iac_code/cli/main.py:151 msgid "Error: --resume and --continue cannot be used together." msgstr "Erreur : --resume et --continue ne peuvent pas être utilisés ensemble." -#: src/iac_code/cli/main.py:338 +#: src/iac_code/cli/main.py:163 +#, python-brace-format +msgid "Invalid --output-format '{}'. Valid values: {}" +msgstr "--output-format '{}' invalide. Valeurs valides : {}" + +#: src/iac_code/cli/main.py:361 msgid "Run iac-code as an ACP server." msgstr "Exécuter iac-code comme serveur ACP." -#: src/iac_code/cli/main.py:340 +#: src/iac_code/cli/main.py:363 msgid "Transport type: stdio or http" msgstr "Type de transport : stdio ou http" -#: src/iac_code/cli/main.py:341 +#: src/iac_code/cli/main.py:364 msgid "HTTP server port" msgstr "Port du serveur HTTP" -#: src/iac_code/cli/main.py:342 src/iac_code/cli/main.py:558 +#: src/iac_code/cli/main.py:365 src/iac_code/cli/main.py:581 msgid "HTTP server host" msgstr "Hôte du serveur HTTP" -#: src/iac_code/cli/main.py:554 +#: src/iac_code/cli/main.py:577 msgid "Run iac-code as an A2A 1.0 server." msgstr "Exécute iac-code en tant que serveur A2A 1.0." -#: src/iac_code/cli/main.py:557 +#: src/iac_code/cli/main.py:580 msgid "YAML config file for A2A server options" msgstr "Fichier de configuration YAML pour les options du serveur A2A" -#: src/iac_code/cli/main.py:561 +#: src/iac_code/cli/main.py:584 msgid "" "HTTP server port. 41242 is the iac-code default inspired by Gemini CLI, " "not a registered A2A port." @@ -301,7 +358,7 @@ msgstr "" "Port du serveur HTTP. 41242 est la valeur par défaut d'iac-code inspirée " "de Gemini CLI, pas un port A2A enregistré." -#: src/iac_code/cli/main.py:566 +#: src/iac_code/cli/main.py:589 msgid "" "A2A transport: http, stdio, unix, websocket, grpc, grpc-jsonrpc, or " "redis-streams" @@ -309,7 +366,7 @@ msgstr "" "Transport A2A : http, stdio, unix, websocket, grpc, grpc-jsonrpc ou " "redis-streams" -#: src/iac_code/cli/main.py:572 +#: src/iac_code/cli/main.py:595 msgid "" "Expose A2A thinking signal types; repeat for multiple. Values: raw-" "thinking, tool-trace." @@ -317,7 +374,7 @@ msgstr "" "Expose les types de signal de thinking A2A ; répétez pour en fournir " "plusieurs. Valeurs : raw-thinking, tool-trace." -#: src/iac_code/cli/main.py:623 +#: src/iac_code/cli/main.py:646 msgid "" "A2A server dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -325,228 +382,228 @@ msgstr "" "Les dépendances du serveur A2A sont manquantes. Installez-les avec : pip " "install 'iac-code[a2a]'" -#: src/iac_code/cli/main.py:755 +#: src/iac_code/cli/main.py:778 msgid "Send a prompt to an A2A JSON-RPC endpoint." msgstr "Envoie un prompt à un point de terminaison JSON-RPC A2A." -#: src/iac_code/cli/main.py:758 src/iac_code/cli/main.py:928 -#: src/iac_code/cli/main.py:981 src/iac_code/cli/main.py:1041 -#: src/iac_code/cli/main.py:1091 src/iac_code/cli/main.py:1140 -#: src/iac_code/cli/main.py:1219 src/iac_code/cli/main.py:1276 -#: src/iac_code/cli/main.py:1332 src/iac_code/cli/main.py:1389 +#: src/iac_code/cli/main.py:781 src/iac_code/cli/main.py:951 +#: src/iac_code/cli/main.py:1004 src/iac_code/cli/main.py:1064 +#: src/iac_code/cli/main.py:1114 src/iac_code/cli/main.py:1163 +#: src/iac_code/cli/main.py:1242 src/iac_code/cli/main.py:1299 +#: src/iac_code/cli/main.py:1355 src/iac_code/cli/main.py:1412 msgid "A2A JSON-RPC endpoint URL" msgstr "URL du point de terminaison JSON-RPC A2A" -#: src/iac_code/cli/main.py:759 src/iac_code/cli/main.py:1434 +#: src/iac_code/cli/main.py:782 src/iac_code/cli/main.py:1457 msgid "Route spec: name=url;skills=skill1,skill2;tags=tag1,tag2" msgstr "Spécification de route : name=url;skills=skill1,skill2;tags=tag1,tag2" -#: src/iac_code/cli/main.py:760 +#: src/iac_code/cli/main.py:783 msgid "Named A2A route to call" msgstr "Route A2A nommée à appeler" -#: src/iac_code/cli/main.py:761 +#: src/iac_code/cli/main.py:784 msgid "Prompt to send" msgstr "Prompt à envoyer" -#: src/iac_code/cli/main.py:762 +#: src/iac_code/cli/main.py:785 msgid "Working directory metadata to send with the request" msgstr "Métadonnées du répertoire de travail à envoyer avec la requête" -#: src/iac_code/cli/main.py:763 +#: src/iac_code/cli/main.py:786 msgid "A2A context ID to continue" msgstr "ID de contexte A2A à poursuivre" -#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:859 -#: src/iac_code/cli/main.py:931 src/iac_code/cli/main.py:988 -#: src/iac_code/cli/main.py:1043 src/iac_code/cli/main.py:1093 -#: src/iac_code/cli/main.py:1147 src/iac_code/cli/main.py:1222 -#: src/iac_code/cli/main.py:1280 src/iac_code/cli/main.py:1335 -#: src/iac_code/cli/main.py:1390 +#: src/iac_code/cli/main.py:787 src/iac_code/cli/main.py:882 +#: src/iac_code/cli/main.py:954 src/iac_code/cli/main.py:1011 +#: src/iac_code/cli/main.py:1066 src/iac_code/cli/main.py:1116 +#: src/iac_code/cli/main.py:1170 src/iac_code/cli/main.py:1245 +#: src/iac_code/cli/main.py:1303 src/iac_code/cli/main.py:1358 +#: src/iac_code/cli/main.py:1413 msgid "Bearer token for A2A HTTP requests" msgstr "Jeton Bearer pour les requêtes HTTP A2A" -#: src/iac_code/cli/main.py:765 src/iac_code/cli/main.py:860 -#: src/iac_code/cli/main.py:932 src/iac_code/cli/main.py:989 -#: src/iac_code/cli/main.py:1044 src/iac_code/cli/main.py:1094 -#: src/iac_code/cli/main.py:1148 src/iac_code/cli/main.py:1223 -#: src/iac_code/cli/main.py:1281 src/iac_code/cli/main.py:1336 -#: src/iac_code/cli/main.py:1391 +#: src/iac_code/cli/main.py:788 src/iac_code/cli/main.py:883 +#: src/iac_code/cli/main.py:955 src/iac_code/cli/main.py:1012 +#: src/iac_code/cli/main.py:1067 src/iac_code/cli/main.py:1117 +#: src/iac_code/cli/main.py:1171 src/iac_code/cli/main.py:1246 +#: src/iac_code/cli/main.py:1304 src/iac_code/cli/main.py:1359 +#: src/iac_code/cli/main.py:1414 msgid "Basic auth username for A2A HTTP requests" msgstr "Nom d'utilisateur d'authentification basique pour les requêtes HTTP A2A" -#: src/iac_code/cli/main.py:766 src/iac_code/cli/main.py:861 -#: src/iac_code/cli/main.py:933 src/iac_code/cli/main.py:990 -#: src/iac_code/cli/main.py:1045 src/iac_code/cli/main.py:1095 -#: src/iac_code/cli/main.py:1149 src/iac_code/cli/main.py:1224 -#: src/iac_code/cli/main.py:1282 src/iac_code/cli/main.py:1337 -#: src/iac_code/cli/main.py:1392 +#: src/iac_code/cli/main.py:789 src/iac_code/cli/main.py:884 +#: src/iac_code/cli/main.py:956 src/iac_code/cli/main.py:1013 +#: src/iac_code/cli/main.py:1068 src/iac_code/cli/main.py:1118 +#: src/iac_code/cli/main.py:1172 src/iac_code/cli/main.py:1247 +#: src/iac_code/cli/main.py:1305 src/iac_code/cli/main.py:1360 +#: src/iac_code/cli/main.py:1415 msgid "Basic auth password for A2A HTTP requests" msgstr "Mot de passe d'authentification basique pour les requêtes HTTP A2A" -#: src/iac_code/cli/main.py:767 src/iac_code/cli/main.py:862 -#: src/iac_code/cli/main.py:934 src/iac_code/cli/main.py:991 -#: src/iac_code/cli/main.py:1046 src/iac_code/cli/main.py:1096 -#: src/iac_code/cli/main.py:1150 src/iac_code/cli/main.py:1225 -#: src/iac_code/cli/main.py:1283 src/iac_code/cli/main.py:1338 -#: src/iac_code/cli/main.py:1393 +#: src/iac_code/cli/main.py:790 src/iac_code/cli/main.py:885 +#: src/iac_code/cli/main.py:957 src/iac_code/cli/main.py:1014 +#: src/iac_code/cli/main.py:1069 src/iac_code/cli/main.py:1119 +#: src/iac_code/cli/main.py:1173 src/iac_code/cli/main.py:1248 +#: src/iac_code/cli/main.py:1306 src/iac_code/cli/main.py:1361 +#: src/iac_code/cli/main.py:1416 msgid "API key for A2A HTTP requests" msgstr "Clé d'API pour les requêtes HTTP A2A" -#: src/iac_code/cli/main.py:768 src/iac_code/cli/main.py:863 -#: src/iac_code/cli/main.py:935 src/iac_code/cli/main.py:992 -#: src/iac_code/cli/main.py:1047 src/iac_code/cli/main.py:1097 -#: src/iac_code/cli/main.py:1151 src/iac_code/cli/main.py:1226 -#: src/iac_code/cli/main.py:1284 src/iac_code/cli/main.py:1339 -#: src/iac_code/cli/main.py:1394 +#: src/iac_code/cli/main.py:791 src/iac_code/cli/main.py:886 +#: src/iac_code/cli/main.py:958 src/iac_code/cli/main.py:1015 +#: src/iac_code/cli/main.py:1070 src/iac_code/cli/main.py:1120 +#: src/iac_code/cli/main.py:1174 src/iac_code/cli/main.py:1249 +#: src/iac_code/cli/main.py:1307 src/iac_code/cli/main.py:1362 +#: src/iac_code/cli/main.py:1417 msgid "HTTP header name for A2A API key" msgstr "Nom de l'en-tête HTTP pour la clé d'API A2A" -#: src/iac_code/cli/main.py:773 src/iac_code/cli/main.py:868 +#: src/iac_code/cli/main.py:796 src/iac_code/cli/main.py:891 msgid "Secret used to verify the A2A Agent Card" msgstr "Secret utilisé pour vérifier l'A2A Agent Card" -#: src/iac_code/cli/main.py:778 src/iac_code/cli/main.py:873 +#: src/iac_code/cli/main.py:801 src/iac_code/cli/main.py:896 msgid "Remote JWKS URL used to verify the A2A Agent Card" msgstr "URL JWKS distante utilisée pour vérifier l'A2A Agent Card" -#: src/iac_code/cli/main.py:784 src/iac_code/cli/main.py:879 +#: src/iac_code/cli/main.py:807 src/iac_code/cli/main.py:902 msgid "Require a valid A2A Agent Card signature" msgstr "Exiger une signature valide de l'A2A Agent Card" -#: src/iac_code/cli/main.py:786 +#: src/iac_code/cli/main.py:809 msgid "A2A call timeout in seconds" msgstr "Délai d'expiration de l'appel A2A en secondes" -#: src/iac_code/cli/main.py:787 +#: src/iac_code/cli/main.py:810 msgid "Use A2A streaming message delivery" msgstr "Utiliser la diffusion en continu de messages A2A" -#: src/iac_code/cli/main.py:855 +#: src/iac_code/cli/main.py:878 msgid "Discover an A2A Agent Card." msgstr "Découvre une A2A Agent Card." -#: src/iac_code/cli/main.py:858 +#: src/iac_code/cli/main.py:881 msgid "A2A agent base URL" msgstr "URL de base de l'agent A2A" -#: src/iac_code/cli/main.py:925 +#: src/iac_code/cli/main.py:948 msgid "Get an A2A task." msgstr "Récupère une tâche A2A." -#: src/iac_code/cli/main.py:929 src/iac_code/cli/main.py:1042 -#: src/iac_code/cli/main.py:1092 src/iac_code/cli/main.py:1141 -#: src/iac_code/cli/main.py:1220 src/iac_code/cli/main.py:1277 -#: src/iac_code/cli/main.py:1333 +#: src/iac_code/cli/main.py:952 src/iac_code/cli/main.py:1065 +#: src/iac_code/cli/main.py:1115 src/iac_code/cli/main.py:1164 +#: src/iac_code/cli/main.py:1243 src/iac_code/cli/main.py:1300 +#: src/iac_code/cli/main.py:1356 msgid "A2A task ID" msgstr "ID de tâche A2A" -#: src/iac_code/cli/main.py:930 +#: src/iac_code/cli/main.py:953 msgid "Maximum task history items to return" msgstr "Nombre maximal d'éléments d'historique de tâche à retourner" -#: src/iac_code/cli/main.py:978 +#: src/iac_code/cli/main.py:1001 msgid "List A2A tasks." msgstr "Liste les tâches A2A." -#: src/iac_code/cli/main.py:982 +#: src/iac_code/cli/main.py:1005 msgid "Filter by A2A context ID" msgstr "Filtrer par ID de contexte A2A" -#: src/iac_code/cli/main.py:983 +#: src/iac_code/cli/main.py:1006 msgid "Filter by A2A task state" msgstr "Filtrer par état de tâche A2A" -#: src/iac_code/cli/main.py:984 +#: src/iac_code/cli/main.py:1007 msgid "Maximum tasks to return" msgstr "Nombre maximal de tâches à retourner" -#: src/iac_code/cli/main.py:985 src/iac_code/cli/main.py:1279 +#: src/iac_code/cli/main.py:1008 src/iac_code/cli/main.py:1302 msgid "Pagination token" msgstr "Jeton de pagination" -#: src/iac_code/cli/main.py:986 +#: src/iac_code/cli/main.py:1009 msgid "Include task artifacts" msgstr "Inclure les artefacts de tâche" -#: src/iac_code/cli/main.py:987 +#: src/iac_code/cli/main.py:1010 msgid "Output format: table or json" msgstr "Format de sortie : table ou json" -#: src/iac_code/cli/main.py:1038 +#: src/iac_code/cli/main.py:1061 msgid "Cancel an A2A task." msgstr "Annule une tâche A2A." -#: src/iac_code/cli/main.py:1088 +#: src/iac_code/cli/main.py:1111 msgid "Subscribe to an A2A task event stream." msgstr "S'abonne à un flux d'événements de tâche A2A." -#: src/iac_code/cli/main.py:1137 +#: src/iac_code/cli/main.py:1160 msgid "Create an A2A task push notification config." msgstr "Crée une configuration de notifications push de tâche A2A." -#: src/iac_code/cli/main.py:1142 src/iac_code/cli/main.py:1221 -#: src/iac_code/cli/main.py:1334 +#: src/iac_code/cli/main.py:1165 src/iac_code/cli/main.py:1244 +#: src/iac_code/cli/main.py:1357 msgid "Push config ID" msgstr "ID de configuration push" -#: src/iac_code/cli/main.py:1143 +#: src/iac_code/cli/main.py:1166 msgid "Push callback URL" msgstr "URL de rappel push" -#: src/iac_code/cli/main.py:1144 +#: src/iac_code/cli/main.py:1167 msgid "Notification verification token" msgstr "Jeton de vérification de notification" -#: src/iac_code/cli/main.py:1145 +#: src/iac_code/cli/main.py:1168 msgid "Callback authentication scheme" msgstr "Schéma d'authentification du rappel" -#: src/iac_code/cli/main.py:1146 +#: src/iac_code/cli/main.py:1169 msgid "Callback authentication credentials" msgstr "Identifiants d'authentification du rappel" -#: src/iac_code/cli/main.py:1216 +#: src/iac_code/cli/main.py:1239 msgid "Get an A2A task push notification config." msgstr "Récupère une configuration de notifications push de tâche A2A." -#: src/iac_code/cli/main.py:1273 +#: src/iac_code/cli/main.py:1296 msgid "List A2A task push notification configs." msgstr "Liste les configurations de notifications push de tâches A2A." -#: src/iac_code/cli/main.py:1278 +#: src/iac_code/cli/main.py:1301 msgid "Maximum configs to return" msgstr "Nombre maximal de configurations à retourner" -#: src/iac_code/cli/main.py:1329 +#: src/iac_code/cli/main.py:1352 msgid "Delete an A2A task push notification config." msgstr "Supprime une configuration de notifications push de tâche A2A." -#: src/iac_code/cli/main.py:1386 +#: src/iac_code/cli/main.py:1409 msgid "Get an authenticated extended A2A Agent Card." msgstr "Récupère une A2A Agent Card étendue authentifiée." -#: src/iac_code/cli/main.py:1429 +#: src/iac_code/cli/main.py:1452 msgid "Preview A2A route resolution." msgstr "Aperçu de la résolution des routes A2A." -#: src/iac_code/cli/main.py:1436 +#: src/iac_code/cli/main.py:1459 msgid "Route name to resolve" msgstr "Nom de route à résoudre" -#: src/iac_code/cli/main.py:1437 +#: src/iac_code/cli/main.py:1460 msgid "Skill ID to resolve" msgstr "ID de compétence à résoudre" -#: src/iac_code/cli/main.py:1438 +#: src/iac_code/cli/main.py:1461 msgid "Prompt text used for tag/name route matching" msgstr "Texte du prompt utilisé pour la correspondance des routes par tag/nom" -#: src/iac_code/cli/main.py:1443 +#: src/iac_code/cli/main.py:1466 msgid "Directory for persisted A2A routes" msgstr "Répertoire pour les routes A2A persistées" -#: src/iac_code/cli/main.py:1445 +#: src/iac_code/cli/main.py:1468 msgid "Save the provided routes as a route snapshot" msgstr "Enregistre les routes fournies sous forme d'instantané de routes" @@ -595,7 +652,7 @@ msgid "[conversation id or search term]" msgstr "[identifiant de conversation ou terme de recherche]" #: src/iac_code/commands/auth.py:382 src/iac_code/commands/auth.py:1107 -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Navigate" msgstr "Naviguer" @@ -603,7 +660,7 @@ msgstr "Naviguer" #: src/iac_code/commands/auth.py:562 src/iac_code/commands/auth.py:569 #: src/iac_code/commands/auth.py:604 src/iac_code/commands/auth.py:611 #: src/iac_code/commands/auth.py:632 src/iac_code/commands/auth.py:1107 -#: src/iac_code/commands/auth.py:1319 src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/commands/auth.py:1319 src/iac_code/ui/core/prompt_input.py:557 msgid "Confirm" msgstr "Confirmer" @@ -1132,6 +1189,11 @@ msgstr "" "désactivez le mode QwenPaw (supprimez 'llm_source: qwenpaw' de " "settings.yml)." +#: src/iac_code/services/permissions/loader.py:50 +#, python-brace-format +msgid "Invalid --permission-mode {!r}. Valid values: {}" +msgstr "--permission-mode invalide : {!r}. Valeurs valides : {}" + #: src/iac_code/services/permissions/pipeline.py:54 #: src/iac_code/tools/base.py:199 src/iac_code/tools/bash/bash_tool.py:175 #, python-brace-format @@ -1866,95 +1928,103 @@ msgstr "Non, toujours refuser \"{rule}\" (cette session)" msgid "No, always reject this tool" msgstr "Non, toujours refuser cet outil" -#: src/iac_code/ui/repl.py:369 +#: src/iac_code/ui/repl.py:370 msgid "Press Ctrl+C again to exit." msgstr "Appuyez de nouveau sur Ctrl+C pour quitter." -#: src/iac_code/ui/repl.py:390 +#: src/iac_code/ui/repl.py:395 msgid "Interrupted." msgstr "Interrompu." -#: src/iac_code/ui/repl.py:427 +#: src/iac_code/ui/repl.py:432 msgid "Goodbye!" msgstr "Au revoir !" -#: src/iac_code/ui/repl.py:428 +#: src/iac_code/ui/repl.py:433 msgid "Resume this session with:" msgstr "Pour reprendre cette session :" -#: src/iac_code/ui/repl.py:450 +#: src/iac_code/ui/repl.py:458 msgid "Update now" msgstr "Mettre à jour maintenant" -#: src/iac_code/ui/repl.py:452 +#: src/iac_code/ui/repl.py:460 msgid "Run the shown update command and exit when it succeeds." msgstr "Exécute la commande de mise à jour affichée et quitte en cas de succès." -#: src/iac_code/ui/repl.py:455 +#: src/iac_code/ui/repl.py:463 msgid "Skip" msgstr "Ignorer" -#: src/iac_code/ui/repl.py:457 +#: src/iac_code/ui/repl.py:465 msgid "Continue with the current version for this session." msgstr "Continuer avec la version actuelle pour cette session." -#: src/iac_code/ui/repl.py:460 +#: src/iac_code/ui/repl.py:468 msgid "Skip until next version" msgstr "Ignorer jusqu’à la prochaine version" -#: src/iac_code/ui/repl.py:462 +#: src/iac_code/ui/repl.py:470 msgid "Hide this update until a newer version is available." msgstr "" "Masquer cette mise à jour jusqu’à ce qu’une version plus récente soit " "disponible." -#: src/iac_code/ui/repl.py:481 src/iac_code/ui/repl.py:493 +#: src/iac_code/ui/repl.py:489 src/iac_code/ui/repl.py:501 msgid "Update command failed. Continuing with the current version." msgstr "La commande de mise à jour a échoué. La version actuelle sera conservée." -#: src/iac_code/ui/repl.py:486 +#: src/iac_code/ui/repl.py:494 msgid "Update completed. Restart iac-code to continue." msgstr "Mise à jour terminée. Redémarrez iac-code pour continuer." -#: src/iac_code/ui/repl.py:524 +#: src/iac_code/ui/repl.py:532 msgid "No image in clipboard." msgstr "Aucune image dans le presse-papiers." -#: src/iac_code/ui/repl.py:677 +#: src/iac_code/ui/repl.py:718 +msgid "Usage: !" +msgstr "Utilisation : !" + +#: src/iac_code/ui/repl.py:723 +msgid "Shell command support is unavailable." +msgstr "La prise en charge des commandes shell n'est pas disponible." + +#: src/iac_code/ui/repl.py:787 #, python-brace-format msgid "Unknown skill: ${name}. Type / to list commands and skills." msgstr "" "Compétence inconnue : ${name}. Tapez / pour lister les commandes et les " "compétences." -#: src/iac_code/ui/repl.py:679 +#: src/iac_code/ui/repl.py:789 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "" "Unknown command: /{name}. Type /help for available commands.Commande " "inconnue : /{name}. Saisissez /help pour la liste des commandes." -#: src/iac_code/ui/repl.py:684 +#: src/iac_code/ui/repl.py:794 #, python-brace-format msgid "$ only invokes skills. Use /{name} instead." msgstr "$ n'invoque que des compétences. Utilisez plutôt /{name}." -#: src/iac_code/ui/repl.py:706 src/iac_code/ui/repl.py:751 +#: src/iac_code/ui/repl.py:816 src/iac_code/ui/repl.py:861 #, python-brace-format msgid "Command error: {error}" msgstr "Erreur de commande : {error}" -#: src/iac_code/ui/repl.py:713 +#: src/iac_code/ui/repl.py:823 #, python-brace-format msgid "Command has no handler: {name}" msgstr "Aucun gestionnaire pour la commande : {name}" -#: src/iac_code/ui/repl.py:1018 +#: src/iac_code/ui/repl.py:1128 #, python-brace-format msgid "Session not found: {session_id}" msgstr "Session introuvable : {session_id}" -#: src/iac_code/ui/repl.py:1037 +#: src/iac_code/ui/repl.py:1147 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1965,19 +2035,19 @@ msgstr "" "Pour la reprendre, exécutez :\n" " {cmd}" -#: src/iac_code/ui/repl.py:1076 +#: src/iac_code/ui/repl.py:1186 msgid "This conversation is from a different directory." msgstr "Cette conversation provient d’un autre répertoire." -#: src/iac_code/ui/repl.py:1078 +#: src/iac_code/ui/repl.py:1188 msgid "To resume, run:" msgstr "Pour reprendre, exécutez :" -#: src/iac_code/ui/repl.py:1083 +#: src/iac_code/ui/repl.py:1193 msgid "(Command copied to clipboard)" msgstr "(Commande copiée dans le presse-papiers)" -#: src/iac_code/ui/repl.py:1240 +#: src/iac_code/ui/repl.py:1350 #, python-brace-format msgid "" "Current model {model} does not support image input. Use /model to switch " @@ -1986,12 +2056,12 @@ msgstr "" "Le modèle actuel {model} ne prend pas en charge l’entrée d’image. " "Utilisez /model pour passer à un modèle compatible vision." -#: src/iac_code/ui/repl.py:1249 +#: src/iac_code/ui/repl.py:1359 #, python-brace-format msgid "Image error: {err}" msgstr "Erreur d’image : {err}" -#: src/iac_code/ui/repl.py:1266 +#: src/iac_code/ui/repl.py:1376 msgid "" "Failed to persist image to cache; it will only exist in memory for this " "turn." @@ -2031,15 +2101,15 @@ msgstr "Affichage de la transcription · ctrl+o pour basculer" msgid "No matches found" msgstr "Aucune correspondance" -#: src/iac_code/ui/core/prompt_input.py:337 +#: src/iac_code/ui/core/prompt_input.py:348 msgid "Image in clipboard · ctrl+v to paste" msgstr "Image dans le presse-papiers · ctrl+v pour coller" -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Fill" msgstr "Remplir" -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Dismiss" msgstr "Fermer" diff --git a/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po index c9c73de..fab60c6 100644 --- a/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.3.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-01 16:49+0800\n" +"POT-Creation-Date: 2026-06-01 17:56+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: ja\n" @@ -17,6 +17,11 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" +#: src/iac_code/config.py:164 +#, python-brace-format +msgid "Invalid IAC_CODE_PROVIDER value: {!r}. Valid values (case-insensitive): {}" +msgstr "無効な IAC_CODE_PROVIDER 値: {!r}。有効な値 (大文字と小文字を区別しません): {}" + #: src/iac_code/a2a/transports/base.py:175 msgid "" "Unix domain socket transport is not supported on Windows. Use --transport" @@ -97,7 +102,8 @@ msgstr "デバッグログを無効にしました。" msgid "Usage: /debug [on|off]" msgstr "使用方法:/debug [on|off]" -#: src/iac_code/agent/agent_loop.py:399 src/iac_code/agent/agent_loop.py:414 +#: src/iac_code/agent/agent_loop.py:404 src/iac_code/agent/agent_loop.py:419 +#: src/iac_code/ui/repl.py:757 src/iac_code/ui/repl.py:771 msgid "Permission denied." msgstr "権限が拒否されました。" @@ -119,7 +125,47 @@ msgstr "計画" msgid "Agent" msgstr "エージェント" -#: src/iac_code/cli/headless.py:60 +#: src/iac_code/cli/headless.py:50 +#, python-brace-format +msgid "Tool started: {}" +msgstr "ツール開始: {}" + +#: src/iac_code/cli/headless.py:53 +#, python-brace-format +msgid "Tool failed: {}" +msgstr "ツール失敗: {}" + +#: src/iac_code/cli/headless.py:55 +#, python-brace-format +msgid "Tool finished: {}" +msgstr "ツール完了: {}" + +#: src/iac_code/cli/headless.py:59 +#, python-brace-format +msgid "Child tool failed: {}" +msgstr "子ツール失敗: {}" + +#: src/iac_code/cli/headless.py:61 +#, python-brace-format +msgid "Child tool finished: {}" +msgstr "子ツール完了: {}" + +#: src/iac_code/cli/headless.py:63 +#, python-brace-format +msgid "Child tool started: {}" +msgstr "子ツール開始: {}" + +#: src/iac_code/cli/headless.py:65 +#, python-brace-format +msgid "Stack {}: {} ({:.1f}%)" +msgstr "スタック {}: {} ({:.1f}%)" + +#: src/iac_code/cli/headless.py:71 +#, python-brace-format +msgid "Stack group {}: {} ({}%)" +msgstr "スタックグループ {}: {} ({}%)" + +#: src/iac_code/cli/headless.py:110 #, python-brace-format msgid "" "\n" @@ -185,8 +231,8 @@ msgstr "npmmirror ミラー経由で Git for Windows をインストールしま msgid "YAML config file containing A2A client options" msgstr "A2A クライアントオプションを含む YAML 設定ファイル" -#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1478 -#: src/iac_code/cli/main.py:1839 src/iac_code/cli/main.py:1878 +#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1501 +#: src/iac_code/cli/main.py:1862 src/iac_code/cli/main.py:1901 msgid "" "A2A client dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -208,76 +254,85 @@ msgstr "出力形式:text、json、stream-json" msgid "Maximum agent turns in headless mode" msgstr "ヘッドレスモードでの最大エージェンターン数" -#: src/iac_code/cli/main.py:86 src/iac_code/cli/main.py:343 -#: src/iac_code/cli/main.py:568 +#: src/iac_code/cli/main.py:86 src/iac_code/cli/main.py:366 +#: src/iac_code/cli/main.py:591 msgid "Enable debug logging" msgstr "デバッグログを有効にする" #: src/iac_code/cli/main.py:87 +msgid "Show headless progress on stderr" +msgstr "ヘッドレス進行状況を stderr に表示" + +#: src/iac_code/cli/main.py:88 msgid "Show version and exit" msgstr "バージョンを表示して終了する" -#: src/iac_code/cli/main.py:88 +#: src/iac_code/cli/main.py:89 msgid "Resume a session by ID" msgstr "ID でセッションを再開する" -#: src/iac_code/cli/main.py:89 +#: src/iac_code/cli/main.py:90 msgid "Resume the most recent session" msgstr "直近のセッションを再開する" -#: src/iac_code/cli/main.py:96 src/iac_code/i18n/__init__.py:55 +#: src/iac_code/cli/main.py:97 src/iac_code/i18n/__init__.py:55 msgid "Install completion for the current shell." msgstr "現在の shell に補完をインストールします。" -#: src/iac_code/cli/main.py:104 src/iac_code/i18n/__init__.py:56 +#: src/iac_code/cli/main.py:105 src/iac_code/i18n/__init__.py:56 msgid "" "Show completion for the current shell, to copy it or customize the " "installation." msgstr "現在の shell 向けの補完スクリプトを表示します。コピーしたり、インストールをカスタマイズしたりできます。" -#: src/iac_code/cli/main.py:109 +#: src/iac_code/cli/main.py:110 msgid "" "Comma-separated tool permission patterns to allow, e.g. 'bash(git " "*),write_file'" msgstr "許可するツール権限パターン(カンマ区切り)、例: 'bash(git *),write_file'*),write_file'" -#: src/iac_code/cli/main.py:114 +#: src/iac_code/cli/main.py:115 msgid "Comma-separated tool permission patterns to deny" msgstr "拒否するツール権限パターン(カンマ区切り)" -#: src/iac_code/cli/main.py:119 +#: src/iac_code/cli/main.py:120 msgid "Permission mode: default, accept_edits, bypass_permissions, dont_ask" msgstr "権限モード: default, accept_edits, bypass_permissions, dont_ask" -#: src/iac_code/cli/main.py:150 +#: src/iac_code/cli/main.py:151 msgid "Error: --resume and --continue cannot be used together." msgstr "エラー:--resume と --continue は同時に使用できません。" -#: src/iac_code/cli/main.py:338 +#: src/iac_code/cli/main.py:163 +#, python-brace-format +msgid "Invalid --output-format '{}'. Valid values: {}" +msgstr "無効な --output-format '{}' です。有効な値: {}" + +#: src/iac_code/cli/main.py:361 msgid "Run iac-code as an ACP server." msgstr "iac-code を ACP サーバーとして実行します。" -#: src/iac_code/cli/main.py:340 +#: src/iac_code/cli/main.py:363 msgid "Transport type: stdio or http" msgstr "トランスポートの種類:stdio または http" -#: src/iac_code/cli/main.py:341 +#: src/iac_code/cli/main.py:364 msgid "HTTP server port" msgstr "HTTP サーバーのポート" -#: src/iac_code/cli/main.py:342 src/iac_code/cli/main.py:558 +#: src/iac_code/cli/main.py:365 src/iac_code/cli/main.py:581 msgid "HTTP server host" msgstr "HTTP サーバーのホスト" -#: src/iac_code/cli/main.py:554 +#: src/iac_code/cli/main.py:577 msgid "Run iac-code as an A2A 1.0 server." msgstr "iac-code を A2A 1.0 サーバーとして実行します。" -#: src/iac_code/cli/main.py:557 +#: src/iac_code/cli/main.py:580 msgid "YAML config file for A2A server options" msgstr "A2A サーバーオプション用の YAML 設定ファイル" -#: src/iac_code/cli/main.py:561 +#: src/iac_code/cli/main.py:584 msgid "" "HTTP server port. 41242 is the iac-code default inspired by Gemini CLI, " "not a registered A2A port." @@ -285,246 +340,246 @@ msgstr "" "HTTP サーバーポート。41242 は Gemini CLI に触発された iac-code のデフォルトで、登録済みの A2A " "ポートではありません。" -#: src/iac_code/cli/main.py:566 +#: src/iac_code/cli/main.py:589 msgid "" "A2A transport: http, stdio, unix, websocket, grpc, grpc-jsonrpc, or " "redis-streams" msgstr "A2A トランスポート: http、stdio、unix、websocket、grpc、grpc-jsonrpc、または redis-streams" -#: src/iac_code/cli/main.py:572 +#: src/iac_code/cli/main.py:595 msgid "" "Expose A2A thinking signal types; repeat for multiple. Values: raw-" "thinking, tool-trace." msgstr "A2A thinking 信号タイプを公開します。複数指定するには繰り返します。値:raw-thinking、tool-trace。" -#: src/iac_code/cli/main.py:623 +#: src/iac_code/cli/main.py:646 msgid "" "A2A server dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" msgstr "A2A サーバーの依存関係が不足しています。次のコマンドでインストールしてください: pip install 'iac-code[a2a]'" -#: src/iac_code/cli/main.py:755 +#: src/iac_code/cli/main.py:778 msgid "Send a prompt to an A2A JSON-RPC endpoint." msgstr "A2A JSON-RPC エンドポイントにプロンプトを送信します。" -#: src/iac_code/cli/main.py:758 src/iac_code/cli/main.py:928 -#: src/iac_code/cli/main.py:981 src/iac_code/cli/main.py:1041 -#: src/iac_code/cli/main.py:1091 src/iac_code/cli/main.py:1140 -#: src/iac_code/cli/main.py:1219 src/iac_code/cli/main.py:1276 -#: src/iac_code/cli/main.py:1332 src/iac_code/cli/main.py:1389 +#: src/iac_code/cli/main.py:781 src/iac_code/cli/main.py:951 +#: src/iac_code/cli/main.py:1004 src/iac_code/cli/main.py:1064 +#: src/iac_code/cli/main.py:1114 src/iac_code/cli/main.py:1163 +#: src/iac_code/cli/main.py:1242 src/iac_code/cli/main.py:1299 +#: src/iac_code/cli/main.py:1355 src/iac_code/cli/main.py:1412 msgid "A2A JSON-RPC endpoint URL" msgstr "A2A JSON-RPC エンドポイント URL" -#: src/iac_code/cli/main.py:759 src/iac_code/cli/main.py:1434 +#: src/iac_code/cli/main.py:782 src/iac_code/cli/main.py:1457 msgid "Route spec: name=url;skills=skill1,skill2;tags=tag1,tag2" msgstr "ルート仕様: name=url;skills=skill1,skill2;tags=tag1,tag2" -#: src/iac_code/cli/main.py:760 +#: src/iac_code/cli/main.py:783 msgid "Named A2A route to call" msgstr "呼び出す名前付き A2A ルート" -#: src/iac_code/cli/main.py:761 +#: src/iac_code/cli/main.py:784 msgid "Prompt to send" msgstr "送信するプロンプト" -#: src/iac_code/cli/main.py:762 +#: src/iac_code/cli/main.py:785 msgid "Working directory metadata to send with the request" msgstr "リクエストと共に送信する作業ディレクトリのメタデータ" -#: src/iac_code/cli/main.py:763 +#: src/iac_code/cli/main.py:786 msgid "A2A context ID to continue" msgstr "継続する A2A コンテキスト ID" -#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:859 -#: src/iac_code/cli/main.py:931 src/iac_code/cli/main.py:988 -#: src/iac_code/cli/main.py:1043 src/iac_code/cli/main.py:1093 -#: src/iac_code/cli/main.py:1147 src/iac_code/cli/main.py:1222 -#: src/iac_code/cli/main.py:1280 src/iac_code/cli/main.py:1335 -#: src/iac_code/cli/main.py:1390 +#: src/iac_code/cli/main.py:787 src/iac_code/cli/main.py:882 +#: src/iac_code/cli/main.py:954 src/iac_code/cli/main.py:1011 +#: src/iac_code/cli/main.py:1066 src/iac_code/cli/main.py:1116 +#: src/iac_code/cli/main.py:1170 src/iac_code/cli/main.py:1245 +#: src/iac_code/cli/main.py:1303 src/iac_code/cli/main.py:1358 +#: src/iac_code/cli/main.py:1413 msgid "Bearer token for A2A HTTP requests" msgstr "A2A HTTP リクエスト用の Bearer トークン" -#: src/iac_code/cli/main.py:765 src/iac_code/cli/main.py:860 -#: src/iac_code/cli/main.py:932 src/iac_code/cli/main.py:989 -#: src/iac_code/cli/main.py:1044 src/iac_code/cli/main.py:1094 -#: src/iac_code/cli/main.py:1148 src/iac_code/cli/main.py:1223 -#: src/iac_code/cli/main.py:1281 src/iac_code/cli/main.py:1336 -#: src/iac_code/cli/main.py:1391 +#: src/iac_code/cli/main.py:788 src/iac_code/cli/main.py:883 +#: src/iac_code/cli/main.py:955 src/iac_code/cli/main.py:1012 +#: src/iac_code/cli/main.py:1067 src/iac_code/cli/main.py:1117 +#: src/iac_code/cli/main.py:1171 src/iac_code/cli/main.py:1246 +#: src/iac_code/cli/main.py:1304 src/iac_code/cli/main.py:1359 +#: src/iac_code/cli/main.py:1414 msgid "Basic auth username for A2A HTTP requests" msgstr "A2A HTTP リクエスト用の Basic 認証ユーザー名" -#: src/iac_code/cli/main.py:766 src/iac_code/cli/main.py:861 -#: src/iac_code/cli/main.py:933 src/iac_code/cli/main.py:990 -#: src/iac_code/cli/main.py:1045 src/iac_code/cli/main.py:1095 -#: src/iac_code/cli/main.py:1149 src/iac_code/cli/main.py:1224 -#: src/iac_code/cli/main.py:1282 src/iac_code/cli/main.py:1337 -#: src/iac_code/cli/main.py:1392 +#: src/iac_code/cli/main.py:789 src/iac_code/cli/main.py:884 +#: src/iac_code/cli/main.py:956 src/iac_code/cli/main.py:1013 +#: src/iac_code/cli/main.py:1068 src/iac_code/cli/main.py:1118 +#: src/iac_code/cli/main.py:1172 src/iac_code/cli/main.py:1247 +#: src/iac_code/cli/main.py:1305 src/iac_code/cli/main.py:1360 +#: src/iac_code/cli/main.py:1415 msgid "Basic auth password for A2A HTTP requests" msgstr "A2A HTTP リクエスト用の Basic 認証パスワード" -#: src/iac_code/cli/main.py:767 src/iac_code/cli/main.py:862 -#: src/iac_code/cli/main.py:934 src/iac_code/cli/main.py:991 -#: src/iac_code/cli/main.py:1046 src/iac_code/cli/main.py:1096 -#: src/iac_code/cli/main.py:1150 src/iac_code/cli/main.py:1225 -#: src/iac_code/cli/main.py:1283 src/iac_code/cli/main.py:1338 -#: src/iac_code/cli/main.py:1393 +#: src/iac_code/cli/main.py:790 src/iac_code/cli/main.py:885 +#: src/iac_code/cli/main.py:957 src/iac_code/cli/main.py:1014 +#: src/iac_code/cli/main.py:1069 src/iac_code/cli/main.py:1119 +#: src/iac_code/cli/main.py:1173 src/iac_code/cli/main.py:1248 +#: src/iac_code/cli/main.py:1306 src/iac_code/cli/main.py:1361 +#: src/iac_code/cli/main.py:1416 msgid "API key for A2A HTTP requests" msgstr "A2A HTTP リクエスト用の API キー" -#: src/iac_code/cli/main.py:768 src/iac_code/cli/main.py:863 -#: src/iac_code/cli/main.py:935 src/iac_code/cli/main.py:992 -#: src/iac_code/cli/main.py:1047 src/iac_code/cli/main.py:1097 -#: src/iac_code/cli/main.py:1151 src/iac_code/cli/main.py:1226 -#: src/iac_code/cli/main.py:1284 src/iac_code/cli/main.py:1339 -#: src/iac_code/cli/main.py:1394 +#: src/iac_code/cli/main.py:791 src/iac_code/cli/main.py:886 +#: src/iac_code/cli/main.py:958 src/iac_code/cli/main.py:1015 +#: src/iac_code/cli/main.py:1070 src/iac_code/cli/main.py:1120 +#: src/iac_code/cli/main.py:1174 src/iac_code/cli/main.py:1249 +#: src/iac_code/cli/main.py:1307 src/iac_code/cli/main.py:1362 +#: src/iac_code/cli/main.py:1417 msgid "HTTP header name for A2A API key" msgstr "A2A API キー用の HTTP ヘッダー名" -#: src/iac_code/cli/main.py:773 src/iac_code/cli/main.py:868 +#: src/iac_code/cli/main.py:796 src/iac_code/cli/main.py:891 msgid "Secret used to verify the A2A Agent Card" msgstr "A2A Agent Card の検証に使用するシークレット" -#: src/iac_code/cli/main.py:778 src/iac_code/cli/main.py:873 +#: src/iac_code/cli/main.py:801 src/iac_code/cli/main.py:896 msgid "Remote JWKS URL used to verify the A2A Agent Card" msgstr "A2A Agent Card の検証に使用するリモート JWKS URL" -#: src/iac_code/cli/main.py:784 src/iac_code/cli/main.py:879 +#: src/iac_code/cli/main.py:807 src/iac_code/cli/main.py:902 msgid "Require a valid A2A Agent Card signature" msgstr "有効な A2A Agent Card 署名を要求します" -#: src/iac_code/cli/main.py:786 +#: src/iac_code/cli/main.py:809 msgid "A2A call timeout in seconds" msgstr "A2A 呼び出しのタイムアウト(秒)" -#: src/iac_code/cli/main.py:787 +#: src/iac_code/cli/main.py:810 msgid "Use A2A streaming message delivery" msgstr "A2A ストリーミングメッセージ配信を使用します" -#: src/iac_code/cli/main.py:855 +#: src/iac_code/cli/main.py:878 msgid "Discover an A2A Agent Card." msgstr "A2A Agent Card を検出します。" -#: src/iac_code/cli/main.py:858 +#: src/iac_code/cli/main.py:881 msgid "A2A agent base URL" msgstr "A2A エージェントのベース URL" -#: src/iac_code/cli/main.py:925 +#: src/iac_code/cli/main.py:948 msgid "Get an A2A task." msgstr "A2A タスクを取得します。" -#: src/iac_code/cli/main.py:929 src/iac_code/cli/main.py:1042 -#: src/iac_code/cli/main.py:1092 src/iac_code/cli/main.py:1141 -#: src/iac_code/cli/main.py:1220 src/iac_code/cli/main.py:1277 -#: src/iac_code/cli/main.py:1333 +#: src/iac_code/cli/main.py:952 src/iac_code/cli/main.py:1065 +#: src/iac_code/cli/main.py:1115 src/iac_code/cli/main.py:1164 +#: src/iac_code/cli/main.py:1243 src/iac_code/cli/main.py:1300 +#: src/iac_code/cli/main.py:1356 msgid "A2A task ID" msgstr "A2A タスク ID" -#: src/iac_code/cli/main.py:930 +#: src/iac_code/cli/main.py:953 msgid "Maximum task history items to return" msgstr "返却するタスク履歴項目の最大数" -#: src/iac_code/cli/main.py:978 +#: src/iac_code/cli/main.py:1001 msgid "List A2A tasks." msgstr "A2A タスクを一覧表示します。" -#: src/iac_code/cli/main.py:982 +#: src/iac_code/cli/main.py:1005 msgid "Filter by A2A context ID" msgstr "A2A コンテキスト ID でフィルタリングします" -#: src/iac_code/cli/main.py:983 +#: src/iac_code/cli/main.py:1006 msgid "Filter by A2A task state" msgstr "A2A タスク状態でフィルタリングします" -#: src/iac_code/cli/main.py:984 +#: src/iac_code/cli/main.py:1007 msgid "Maximum tasks to return" msgstr "返却するタスクの最大数" -#: src/iac_code/cli/main.py:985 src/iac_code/cli/main.py:1279 +#: src/iac_code/cli/main.py:1008 src/iac_code/cli/main.py:1302 msgid "Pagination token" msgstr "ページネーショントークン" -#: src/iac_code/cli/main.py:986 +#: src/iac_code/cli/main.py:1009 msgid "Include task artifacts" msgstr "タスクアーティファクトを含めます" -#: src/iac_code/cli/main.py:987 +#: src/iac_code/cli/main.py:1010 msgid "Output format: table or json" msgstr "出力形式: table または json" -#: src/iac_code/cli/main.py:1038 +#: src/iac_code/cli/main.py:1061 msgid "Cancel an A2A task." msgstr "A2A タスクをキャンセルします。" -#: src/iac_code/cli/main.py:1088 +#: src/iac_code/cli/main.py:1111 msgid "Subscribe to an A2A task event stream." msgstr "A2A タスクのイベントストリームを購読します。" -#: src/iac_code/cli/main.py:1137 +#: src/iac_code/cli/main.py:1160 msgid "Create an A2A task push notification config." msgstr "A2A タスクプッシュ通知設定を作成します。" -#: src/iac_code/cli/main.py:1142 src/iac_code/cli/main.py:1221 -#: src/iac_code/cli/main.py:1334 +#: src/iac_code/cli/main.py:1165 src/iac_code/cli/main.py:1244 +#: src/iac_code/cli/main.py:1357 msgid "Push config ID" msgstr "プッシュ設定 ID" -#: src/iac_code/cli/main.py:1143 +#: src/iac_code/cli/main.py:1166 msgid "Push callback URL" msgstr "プッシュコールバック URL" -#: src/iac_code/cli/main.py:1144 +#: src/iac_code/cli/main.py:1167 msgid "Notification verification token" msgstr "通知検証トークン" -#: src/iac_code/cli/main.py:1145 +#: src/iac_code/cli/main.py:1168 msgid "Callback authentication scheme" msgstr "コールバック認証方式" -#: src/iac_code/cli/main.py:1146 +#: src/iac_code/cli/main.py:1169 msgid "Callback authentication credentials" msgstr "コールバック認証資格情報" -#: src/iac_code/cli/main.py:1216 +#: src/iac_code/cli/main.py:1239 msgid "Get an A2A task push notification config." msgstr "A2A タスクプッシュ通知設定を取得します。" -#: src/iac_code/cli/main.py:1273 +#: src/iac_code/cli/main.py:1296 msgid "List A2A task push notification configs." msgstr "A2A タスクプッシュ通知設定を一覧表示します。" -#: src/iac_code/cli/main.py:1278 +#: src/iac_code/cli/main.py:1301 msgid "Maximum configs to return" msgstr "返却する設定の最大数" -#: src/iac_code/cli/main.py:1329 +#: src/iac_code/cli/main.py:1352 msgid "Delete an A2A task push notification config." msgstr "A2A タスクプッシュ通知設定を削除します。" -#: src/iac_code/cli/main.py:1386 +#: src/iac_code/cli/main.py:1409 msgid "Get an authenticated extended A2A Agent Card." msgstr "認証済みの拡張 A2A Agent Card を取得します。" -#: src/iac_code/cli/main.py:1429 +#: src/iac_code/cli/main.py:1452 msgid "Preview A2A route resolution." msgstr "A2A ルート解決をプレビューします。" -#: src/iac_code/cli/main.py:1436 +#: src/iac_code/cli/main.py:1459 msgid "Route name to resolve" msgstr "解決するルート名" -#: src/iac_code/cli/main.py:1437 +#: src/iac_code/cli/main.py:1460 msgid "Skill ID to resolve" msgstr "解決するスキル ID" -#: src/iac_code/cli/main.py:1438 +#: src/iac_code/cli/main.py:1461 msgid "Prompt text used for tag/name route matching" msgstr "タグ/名前ルートのマッチングに使用するプロンプトテキスト" -#: src/iac_code/cli/main.py:1443 +#: src/iac_code/cli/main.py:1466 msgid "Directory for persisted A2A routes" msgstr "永続化された A2A ルート用のディレクトリ" -#: src/iac_code/cli/main.py:1445 +#: src/iac_code/cli/main.py:1468 msgid "Save the provided routes as a route snapshot" msgstr "指定されたルートをルートスナップショットとして保存します" @@ -573,7 +628,7 @@ msgid "[conversation id or search term]" msgstr "[会話 ID または検索語]" #: src/iac_code/commands/auth.py:382 src/iac_code/commands/auth.py:1107 -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Navigate" msgstr "移動" @@ -581,7 +636,7 @@ msgstr "移動" #: src/iac_code/commands/auth.py:562 src/iac_code/commands/auth.py:569 #: src/iac_code/commands/auth.py:604 src/iac_code/commands/auth.py:611 #: src/iac_code/commands/auth.py:632 src/iac_code/commands/auth.py:1107 -#: src/iac_code/commands/auth.py:1319 src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/commands/auth.py:1319 src/iac_code/ui/core/prompt_input.py:557 msgid "Confirm" msgstr "確認" @@ -1099,6 +1154,11 @@ msgstr "" "対処法:QwenPaw でサポートされているプロバイダーに切り替えるか、QwenPaw モードを無効にしてください(settings.yml から" " 'llm_source: qwenpaw' を削除)。" +#: src/iac_code/services/permissions/loader.py:50 +#, python-brace-format +msgid "Invalid --permission-mode {!r}. Valid values: {}" +msgstr "無効な --permission-mode {!r} です。有効な値: {}" + #: src/iac_code/services/permissions/pipeline.py:54 #: src/iac_code/tools/base.py:199 src/iac_code/tools/bash/bash_tool.py:175 #, python-brace-format @@ -1813,91 +1873,99 @@ msgstr "いいえ、常に \"{rule}\" を拒否(このセッション)" msgid "No, always reject this tool" msgstr "いいえ、このツールは常に拒否" -#: src/iac_code/ui/repl.py:369 +#: src/iac_code/ui/repl.py:370 msgid "Press Ctrl+C again to exit." msgstr "終了するには Ctrl+C をもう一度押してください。" -#: src/iac_code/ui/repl.py:390 +#: src/iac_code/ui/repl.py:395 msgid "Interrupted." msgstr "中断しました。" -#: src/iac_code/ui/repl.py:427 +#: src/iac_code/ui/repl.py:432 msgid "Goodbye!" msgstr "さようなら。" -#: src/iac_code/ui/repl.py:428 +#: src/iac_code/ui/repl.py:433 msgid "Resume this session with:" msgstr "このセッションを再開するには次を実行してください:" -#: src/iac_code/ui/repl.py:450 +#: src/iac_code/ui/repl.py:458 msgid "Update now" msgstr "今すぐ更新" -#: src/iac_code/ui/repl.py:452 +#: src/iac_code/ui/repl.py:460 msgid "Run the shown update command and exit when it succeeds." msgstr "表示された更新コマンドを実行し、成功したら終了します。" -#: src/iac_code/ui/repl.py:455 +#: src/iac_code/ui/repl.py:463 msgid "Skip" msgstr "スキップ" -#: src/iac_code/ui/repl.py:457 +#: src/iac_code/ui/repl.py:465 msgid "Continue with the current version for this session." msgstr "このセッションでは現在のバージョンを使い続けます。" -#: src/iac_code/ui/repl.py:460 +#: src/iac_code/ui/repl.py:468 msgid "Skip until next version" msgstr "次のバージョンまでスキップ" -#: src/iac_code/ui/repl.py:462 +#: src/iac_code/ui/repl.py:470 msgid "Hide this update until a newer version is available." msgstr "より新しいバージョンが利用可能になるまで、この更新を非表示にします。" -#: src/iac_code/ui/repl.py:481 src/iac_code/ui/repl.py:493 +#: src/iac_code/ui/repl.py:489 src/iac_code/ui/repl.py:501 msgid "Update command failed. Continuing with the current version." msgstr "更新コマンドに失敗しました。現在のバージョンで続行します。" -#: src/iac_code/ui/repl.py:486 +#: src/iac_code/ui/repl.py:494 msgid "Update completed. Restart iac-code to continue." msgstr "更新が完了しました。続行するには iac-code を再起動してください。" -#: src/iac_code/ui/repl.py:524 +#: src/iac_code/ui/repl.py:532 msgid "No image in clipboard." msgstr "クリップボードに画像がありません。" -#: src/iac_code/ui/repl.py:677 +#: src/iac_code/ui/repl.py:718 +msgid "Usage: !" +msgstr "使用方法: !" + +#: src/iac_code/ui/repl.py:723 +msgid "Shell command support is unavailable." +msgstr "シェルコマンドのサポートは利用できません。" + +#: src/iac_code/ui/repl.py:787 #, python-brace-format msgid "Unknown skill: ${name}. Type / to list commands and skills." msgstr "不明なスキル: ${name}。/ を入力するとコマンドとスキルを一覧表示します。" -#: src/iac_code/ui/repl.py:679 +#: src/iac_code/ui/repl.py:789 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "" "Unknown command: /{name}. Type /help for available " "commands.不明なコマンドです:/{name}。利用可能なコマンドは /help を入力してください。" -#: src/iac_code/ui/repl.py:684 +#: src/iac_code/ui/repl.py:794 #, python-brace-format msgid "$ only invokes skills. Use /{name} instead." msgstr "$ はスキルのみを呼び出します。代わりに /{name} を使用してください。" -#: src/iac_code/ui/repl.py:706 src/iac_code/ui/repl.py:751 +#: src/iac_code/ui/repl.py:816 src/iac_code/ui/repl.py:861 #, python-brace-format msgid "Command error: {error}" msgstr "コマンドエラー:{error}" -#: src/iac_code/ui/repl.py:713 +#: src/iac_code/ui/repl.py:823 #, python-brace-format msgid "Command has no handler: {name}" msgstr "ハンドラーがないコマンドです:{name}" -#: src/iac_code/ui/repl.py:1018 +#: src/iac_code/ui/repl.py:1128 #, python-brace-format msgid "Session not found: {session_id}" msgstr "セッションが見つかりません:{session_id}" -#: src/iac_code/ui/repl.py:1037 +#: src/iac_code/ui/repl.py:1147 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1908,31 +1976,31 @@ msgstr "" "再開するには次を実行してください:\n" " {cmd}" -#: src/iac_code/ui/repl.py:1076 +#: src/iac_code/ui/repl.py:1186 msgid "This conversation is from a different directory." msgstr "この会話は別のディレクトリ由来です。" -#: src/iac_code/ui/repl.py:1078 +#: src/iac_code/ui/repl.py:1188 msgid "To resume, run:" msgstr "再開するには次を実行してください:" -#: src/iac_code/ui/repl.py:1083 +#: src/iac_code/ui/repl.py:1193 msgid "(Command copied to clipboard)" msgstr "(コマンドをクリップボードにコピーしました)" -#: src/iac_code/ui/repl.py:1240 +#: src/iac_code/ui/repl.py:1350 #, python-brace-format msgid "" "Current model {model} does not support image input. Use /model to switch " "to a vision-capable model." msgstr "現在のモデル {model} は画像入力をサポートしていません。/model を使用してビジョン対応モデルに切り替えてください。" -#: src/iac_code/ui/repl.py:1249 +#: src/iac_code/ui/repl.py:1359 #, python-brace-format msgid "Image error: {err}" msgstr "画像エラー:{err}" -#: src/iac_code/ui/repl.py:1266 +#: src/iac_code/ui/repl.py:1376 msgid "" "Failed to persist image to cache; it will only exist in memory for this " "turn." @@ -1970,15 +2038,15 @@ msgstr "トランスクリプトを表示中 · ctrl+o で切り替え" msgid "No matches found" msgstr "一致する項目がありません" -#: src/iac_code/ui/core/prompt_input.py:337 +#: src/iac_code/ui/core/prompt_input.py:348 msgid "Image in clipboard · ctrl+v to paste" msgstr "クリップボードに画像 · ctrl+v で貼り付け" -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Fill" msgstr "入力" -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Dismiss" msgstr "閉じる" diff --git a/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po index dc297f8..a1fbd13 100644 --- a/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.3.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-01 16:49+0800\n" +"POT-Creation-Date: 2026-06-01 17:56+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: pt\n" @@ -17,6 +17,13 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" +#: src/iac_code/config.py:164 +#, python-brace-format +msgid "Invalid IAC_CODE_PROVIDER value: {!r}. Valid values (case-insensitive): {}" +msgstr "" +"Valor inválido de IAC_CODE_PROVIDER: {!r}. Valores válidos (sem " +"diferenciar maiúsculas de minúsculas): {}" + #: src/iac_code/a2a/transports/base.py:175 msgid "" "Unix domain socket transport is not supported on Windows. Use --transport" @@ -101,7 +108,8 @@ msgstr "Debug desativado." msgid "Usage: /debug [on|off]" msgstr "Uso: /debug [on|off]" -#: src/iac_code/agent/agent_loop.py:399 src/iac_code/agent/agent_loop.py:414 +#: src/iac_code/agent/agent_loop.py:404 src/iac_code/agent/agent_loop.py:419 +#: src/iac_code/ui/repl.py:757 src/iac_code/ui/repl.py:771 msgid "Permission denied." msgstr "Permissão negada." @@ -123,7 +131,47 @@ msgstr "Planejar" msgid "Agent" msgstr "Agent" -#: src/iac_code/cli/headless.py:60 +#: src/iac_code/cli/headless.py:50 +#, python-brace-format +msgid "Tool started: {}" +msgstr "Ferramenta iniciada: {}" + +#: src/iac_code/cli/headless.py:53 +#, python-brace-format +msgid "Tool failed: {}" +msgstr "Ferramenta falhou: {}" + +#: src/iac_code/cli/headless.py:55 +#, python-brace-format +msgid "Tool finished: {}" +msgstr "Ferramenta concluída: {}" + +#: src/iac_code/cli/headless.py:59 +#, python-brace-format +msgid "Child tool failed: {}" +msgstr "Ferramenta filha falhou: {}" + +#: src/iac_code/cli/headless.py:61 +#, python-brace-format +msgid "Child tool finished: {}" +msgstr "Ferramenta filha concluída: {}" + +#: src/iac_code/cli/headless.py:63 +#, python-brace-format +msgid "Child tool started: {}" +msgstr "Ferramenta filha iniciada: {}" + +#: src/iac_code/cli/headless.py:65 +#, python-brace-format +msgid "Stack {}: {} ({:.1f}%)" +msgstr "Pilha {}: {} ({:.1f}%)" + +#: src/iac_code/cli/headless.py:71 +#, python-brace-format +msgid "Stack group {}: {} ({}%)" +msgstr "Grupo de pilhas {}: {} ({}%)" + +#: src/iac_code/cli/headless.py:110 #, python-brace-format msgid "" "\n" @@ -191,8 +239,8 @@ msgstr "Instalar Git for Windows pelo espelho npmmirror (somente Windows)." msgid "YAML config file containing A2A client options" msgstr "Arquivo de configuração YAML com opções do cliente A2A" -#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1478 -#: src/iac_code/cli/main.py:1839 src/iac_code/cli/main.py:1878 +#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1501 +#: src/iac_code/cli/main.py:1862 src/iac_code/cli/main.py:1901 msgid "" "A2A client dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -216,28 +264,32 @@ msgstr "Formato de saída: text, json, stream-json" msgid "Maximum agent turns in headless mode" msgstr "Número máximo de passos do agent em modo headless" -#: src/iac_code/cli/main.py:86 src/iac_code/cli/main.py:343 -#: src/iac_code/cli/main.py:568 +#: src/iac_code/cli/main.py:86 src/iac_code/cli/main.py:366 +#: src/iac_code/cli/main.py:591 msgid "Enable debug logging" msgstr "Ativar debug" #: src/iac_code/cli/main.py:87 +msgid "Show headless progress on stderr" +msgstr "Mostrar progresso do modo headless no stderr" + +#: src/iac_code/cli/main.py:88 msgid "Show version and exit" msgstr "Mostrar versão e sair" -#: src/iac_code/cli/main.py:88 +#: src/iac_code/cli/main.py:89 msgid "Resume a session by ID" msgstr "Retomar uma sessão pelo ID" -#: src/iac_code/cli/main.py:89 +#: src/iac_code/cli/main.py:90 msgid "Resume the most recent session" msgstr "Retomar a sessão mais recente" -#: src/iac_code/cli/main.py:96 src/iac_code/i18n/__init__.py:55 +#: src/iac_code/cli/main.py:97 src/iac_code/i18n/__init__.py:55 msgid "Install completion for the current shell." msgstr "Instalar completion para o shell atual." -#: src/iac_code/cli/main.py:104 src/iac_code/i18n/__init__.py:56 +#: src/iac_code/cli/main.py:105 src/iac_code/i18n/__init__.py:56 msgid "" "Show completion for the current shell, to copy it or customize the " "installation." @@ -245,7 +297,7 @@ msgstr "" "Exibir o completion do shell atual, para copiar ou personalizar a " "instalação." -#: src/iac_code/cli/main.py:109 +#: src/iac_code/cli/main.py:110 msgid "" "Comma-separated tool permission patterns to allow, e.g. 'bash(git " "*),write_file'" @@ -253,43 +305,48 @@ msgstr "" "Padrões de permissão de ferramentas a permitir (separados por vírgula), " "ex. 'bash(git *),write_file'*),write_file'" -#: src/iac_code/cli/main.py:114 +#: src/iac_code/cli/main.py:115 msgid "Comma-separated tool permission patterns to deny" msgstr "Padrões de permissão de ferramentas a negar (separados por vírgula)" -#: src/iac_code/cli/main.py:119 +#: src/iac_code/cli/main.py:120 msgid "Permission mode: default, accept_edits, bypass_permissions, dont_ask" msgstr "Modo de permissão: default, accept_edits, bypass_permissions, dont_ask" -#: src/iac_code/cli/main.py:150 +#: src/iac_code/cli/main.py:151 msgid "Error: --resume and --continue cannot be used together." msgstr "Erro: --resume e --continue não podem ser usados juntos." -#: src/iac_code/cli/main.py:338 +#: src/iac_code/cli/main.py:163 +#, python-brace-format +msgid "Invalid --output-format '{}'. Valid values: {}" +msgstr "--output-format inválido '{}'. Valores válidos: {}" + +#: src/iac_code/cli/main.py:361 msgid "Run iac-code as an ACP server." msgstr "Executar o iac-code como servidor ACP." -#: src/iac_code/cli/main.py:340 +#: src/iac_code/cli/main.py:363 msgid "Transport type: stdio or http" msgstr "Tipo de transporte: stdio ou http" -#: src/iac_code/cli/main.py:341 +#: src/iac_code/cli/main.py:364 msgid "HTTP server port" msgstr "Porta do servidor HTTP" -#: src/iac_code/cli/main.py:342 src/iac_code/cli/main.py:558 +#: src/iac_code/cli/main.py:365 src/iac_code/cli/main.py:581 msgid "HTTP server host" msgstr "Host do servidor HTTP" -#: src/iac_code/cli/main.py:554 +#: src/iac_code/cli/main.py:577 msgid "Run iac-code as an A2A 1.0 server." msgstr "Executa o iac-code como servidor A2A 1.0." -#: src/iac_code/cli/main.py:557 +#: src/iac_code/cli/main.py:580 msgid "YAML config file for A2A server options" msgstr "Arquivo de configuração YAML para opções do servidor A2A" -#: src/iac_code/cli/main.py:561 +#: src/iac_code/cli/main.py:584 msgid "" "HTTP server port. 41242 is the iac-code default inspired by Gemini CLI, " "not a registered A2A port." @@ -297,7 +354,7 @@ msgstr "" "Porta do servidor HTTP. 41242 é o padrão do iac-code inspirado no Gemini " "CLI, não uma porta A2A registrada." -#: src/iac_code/cli/main.py:566 +#: src/iac_code/cli/main.py:589 msgid "" "A2A transport: http, stdio, unix, websocket, grpc, grpc-jsonrpc, or " "redis-streams" @@ -305,7 +362,7 @@ msgstr "" "Transporte A2A: http, stdio, unix, websocket, grpc, grpc-jsonrpc ou " "redis-streams" -#: src/iac_code/cli/main.py:572 +#: src/iac_code/cli/main.py:595 msgid "" "Expose A2A thinking signal types; repeat for multiple. Values: raw-" "thinking, tool-trace." @@ -313,7 +370,7 @@ msgstr "" "Expõe tipos de sinal de thinking A2A; repita para múltiplos. Valores: " "raw-thinking, tool-trace." -#: src/iac_code/cli/main.py:623 +#: src/iac_code/cli/main.py:646 msgid "" "A2A server dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -321,228 +378,228 @@ msgstr "" "As dependências do servidor A2A estão ausentes. Instale com: pip install " "'iac-code[a2a]'" -#: src/iac_code/cli/main.py:755 +#: src/iac_code/cli/main.py:778 msgid "Send a prompt to an A2A JSON-RPC endpoint." msgstr "Envia um prompt para um endpoint JSON-RPC A2A." -#: src/iac_code/cli/main.py:758 src/iac_code/cli/main.py:928 -#: src/iac_code/cli/main.py:981 src/iac_code/cli/main.py:1041 -#: src/iac_code/cli/main.py:1091 src/iac_code/cli/main.py:1140 -#: src/iac_code/cli/main.py:1219 src/iac_code/cli/main.py:1276 -#: src/iac_code/cli/main.py:1332 src/iac_code/cli/main.py:1389 +#: src/iac_code/cli/main.py:781 src/iac_code/cli/main.py:951 +#: src/iac_code/cli/main.py:1004 src/iac_code/cli/main.py:1064 +#: src/iac_code/cli/main.py:1114 src/iac_code/cli/main.py:1163 +#: src/iac_code/cli/main.py:1242 src/iac_code/cli/main.py:1299 +#: src/iac_code/cli/main.py:1355 src/iac_code/cli/main.py:1412 msgid "A2A JSON-RPC endpoint URL" msgstr "URL do endpoint JSON-RPC A2A" -#: src/iac_code/cli/main.py:759 src/iac_code/cli/main.py:1434 +#: src/iac_code/cli/main.py:782 src/iac_code/cli/main.py:1457 msgid "Route spec: name=url;skills=skill1,skill2;tags=tag1,tag2" msgstr "Especificação de rota: name=url;skills=skill1,skill2;tags=tag1,tag2" -#: src/iac_code/cli/main.py:760 +#: src/iac_code/cli/main.py:783 msgid "Named A2A route to call" msgstr "Rota A2A nomeada a chamar" -#: src/iac_code/cli/main.py:761 +#: src/iac_code/cli/main.py:784 msgid "Prompt to send" msgstr "Prompt para enviar" -#: src/iac_code/cli/main.py:762 +#: src/iac_code/cli/main.py:785 msgid "Working directory metadata to send with the request" msgstr "Metadados do diretório de trabalho a enviar com a requisição" -#: src/iac_code/cli/main.py:763 +#: src/iac_code/cli/main.py:786 msgid "A2A context ID to continue" msgstr "ID de contexto A2A para continuar" -#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:859 -#: src/iac_code/cli/main.py:931 src/iac_code/cli/main.py:988 -#: src/iac_code/cli/main.py:1043 src/iac_code/cli/main.py:1093 -#: src/iac_code/cli/main.py:1147 src/iac_code/cli/main.py:1222 -#: src/iac_code/cli/main.py:1280 src/iac_code/cli/main.py:1335 -#: src/iac_code/cli/main.py:1390 +#: src/iac_code/cli/main.py:787 src/iac_code/cli/main.py:882 +#: src/iac_code/cli/main.py:954 src/iac_code/cli/main.py:1011 +#: src/iac_code/cli/main.py:1066 src/iac_code/cli/main.py:1116 +#: src/iac_code/cli/main.py:1170 src/iac_code/cli/main.py:1245 +#: src/iac_code/cli/main.py:1303 src/iac_code/cli/main.py:1358 +#: src/iac_code/cli/main.py:1413 msgid "Bearer token for A2A HTTP requests" msgstr "Token Bearer para requisições HTTP A2A" -#: src/iac_code/cli/main.py:765 src/iac_code/cli/main.py:860 -#: src/iac_code/cli/main.py:932 src/iac_code/cli/main.py:989 -#: src/iac_code/cli/main.py:1044 src/iac_code/cli/main.py:1094 -#: src/iac_code/cli/main.py:1148 src/iac_code/cli/main.py:1223 -#: src/iac_code/cli/main.py:1281 src/iac_code/cli/main.py:1336 -#: src/iac_code/cli/main.py:1391 +#: src/iac_code/cli/main.py:788 src/iac_code/cli/main.py:883 +#: src/iac_code/cli/main.py:955 src/iac_code/cli/main.py:1012 +#: src/iac_code/cli/main.py:1067 src/iac_code/cli/main.py:1117 +#: src/iac_code/cli/main.py:1171 src/iac_code/cli/main.py:1246 +#: src/iac_code/cli/main.py:1304 src/iac_code/cli/main.py:1359 +#: src/iac_code/cli/main.py:1414 msgid "Basic auth username for A2A HTTP requests" msgstr "Nome de usuário de autenticação básica para requisições HTTP A2A" -#: src/iac_code/cli/main.py:766 src/iac_code/cli/main.py:861 -#: src/iac_code/cli/main.py:933 src/iac_code/cli/main.py:990 -#: src/iac_code/cli/main.py:1045 src/iac_code/cli/main.py:1095 -#: src/iac_code/cli/main.py:1149 src/iac_code/cli/main.py:1224 -#: src/iac_code/cli/main.py:1282 src/iac_code/cli/main.py:1337 -#: src/iac_code/cli/main.py:1392 +#: src/iac_code/cli/main.py:789 src/iac_code/cli/main.py:884 +#: src/iac_code/cli/main.py:956 src/iac_code/cli/main.py:1013 +#: src/iac_code/cli/main.py:1068 src/iac_code/cli/main.py:1118 +#: src/iac_code/cli/main.py:1172 src/iac_code/cli/main.py:1247 +#: src/iac_code/cli/main.py:1305 src/iac_code/cli/main.py:1360 +#: src/iac_code/cli/main.py:1415 msgid "Basic auth password for A2A HTTP requests" msgstr "Senha de autenticação básica para requisições HTTP A2A" -#: src/iac_code/cli/main.py:767 src/iac_code/cli/main.py:862 -#: src/iac_code/cli/main.py:934 src/iac_code/cli/main.py:991 -#: src/iac_code/cli/main.py:1046 src/iac_code/cli/main.py:1096 -#: src/iac_code/cli/main.py:1150 src/iac_code/cli/main.py:1225 -#: src/iac_code/cli/main.py:1283 src/iac_code/cli/main.py:1338 -#: src/iac_code/cli/main.py:1393 +#: src/iac_code/cli/main.py:790 src/iac_code/cli/main.py:885 +#: src/iac_code/cli/main.py:957 src/iac_code/cli/main.py:1014 +#: src/iac_code/cli/main.py:1069 src/iac_code/cli/main.py:1119 +#: src/iac_code/cli/main.py:1173 src/iac_code/cli/main.py:1248 +#: src/iac_code/cli/main.py:1306 src/iac_code/cli/main.py:1361 +#: src/iac_code/cli/main.py:1416 msgid "API key for A2A HTTP requests" msgstr "Chave de API para requisições HTTP A2A" -#: src/iac_code/cli/main.py:768 src/iac_code/cli/main.py:863 -#: src/iac_code/cli/main.py:935 src/iac_code/cli/main.py:992 -#: src/iac_code/cli/main.py:1047 src/iac_code/cli/main.py:1097 -#: src/iac_code/cli/main.py:1151 src/iac_code/cli/main.py:1226 -#: src/iac_code/cli/main.py:1284 src/iac_code/cli/main.py:1339 -#: src/iac_code/cli/main.py:1394 +#: src/iac_code/cli/main.py:791 src/iac_code/cli/main.py:886 +#: src/iac_code/cli/main.py:958 src/iac_code/cli/main.py:1015 +#: src/iac_code/cli/main.py:1070 src/iac_code/cli/main.py:1120 +#: src/iac_code/cli/main.py:1174 src/iac_code/cli/main.py:1249 +#: src/iac_code/cli/main.py:1307 src/iac_code/cli/main.py:1362 +#: src/iac_code/cli/main.py:1417 msgid "HTTP header name for A2A API key" msgstr "Nome do cabeçalho HTTP para a chave de API A2A" -#: src/iac_code/cli/main.py:773 src/iac_code/cli/main.py:868 +#: src/iac_code/cli/main.py:796 src/iac_code/cli/main.py:891 msgid "Secret used to verify the A2A Agent Card" msgstr "Segredo usado para verificar o A2A Agent Card" -#: src/iac_code/cli/main.py:778 src/iac_code/cli/main.py:873 +#: src/iac_code/cli/main.py:801 src/iac_code/cli/main.py:896 msgid "Remote JWKS URL used to verify the A2A Agent Card" msgstr "URL JWKS remota usada para verificar o A2A Agent Card" -#: src/iac_code/cli/main.py:784 src/iac_code/cli/main.py:879 +#: src/iac_code/cli/main.py:807 src/iac_code/cli/main.py:902 msgid "Require a valid A2A Agent Card signature" msgstr "Exigir uma assinatura válida do A2A Agent Card" -#: src/iac_code/cli/main.py:786 +#: src/iac_code/cli/main.py:809 msgid "A2A call timeout in seconds" msgstr "Tempo limite da chamada A2A em segundos" -#: src/iac_code/cli/main.py:787 +#: src/iac_code/cli/main.py:810 msgid "Use A2A streaming message delivery" msgstr "Usar entrega de mensagens em streaming A2A" -#: src/iac_code/cli/main.py:855 +#: src/iac_code/cli/main.py:878 msgid "Discover an A2A Agent Card." msgstr "Descobre um A2A Agent Card." -#: src/iac_code/cli/main.py:858 +#: src/iac_code/cli/main.py:881 msgid "A2A agent base URL" msgstr "URL base do agente A2A" -#: src/iac_code/cli/main.py:925 +#: src/iac_code/cli/main.py:948 msgid "Get an A2A task." msgstr "Obtém uma tarefa A2A." -#: src/iac_code/cli/main.py:929 src/iac_code/cli/main.py:1042 -#: src/iac_code/cli/main.py:1092 src/iac_code/cli/main.py:1141 -#: src/iac_code/cli/main.py:1220 src/iac_code/cli/main.py:1277 -#: src/iac_code/cli/main.py:1333 +#: src/iac_code/cli/main.py:952 src/iac_code/cli/main.py:1065 +#: src/iac_code/cli/main.py:1115 src/iac_code/cli/main.py:1164 +#: src/iac_code/cli/main.py:1243 src/iac_code/cli/main.py:1300 +#: src/iac_code/cli/main.py:1356 msgid "A2A task ID" msgstr "ID da tarefa A2A" -#: src/iac_code/cli/main.py:930 +#: src/iac_code/cli/main.py:953 msgid "Maximum task history items to return" msgstr "Número máximo de itens do histórico de tarefas a retornar" -#: src/iac_code/cli/main.py:978 +#: src/iac_code/cli/main.py:1001 msgid "List A2A tasks." msgstr "Lista as tarefas A2A." -#: src/iac_code/cli/main.py:982 +#: src/iac_code/cli/main.py:1005 msgid "Filter by A2A context ID" msgstr "Filtrar por ID de contexto A2A" -#: src/iac_code/cli/main.py:983 +#: src/iac_code/cli/main.py:1006 msgid "Filter by A2A task state" msgstr "Filtrar por estado de tarefa A2A" -#: src/iac_code/cli/main.py:984 +#: src/iac_code/cli/main.py:1007 msgid "Maximum tasks to return" msgstr "Número máximo de tarefas a retornar" -#: src/iac_code/cli/main.py:985 src/iac_code/cli/main.py:1279 +#: src/iac_code/cli/main.py:1008 src/iac_code/cli/main.py:1302 msgid "Pagination token" msgstr "Token de paginação" -#: src/iac_code/cli/main.py:986 +#: src/iac_code/cli/main.py:1009 msgid "Include task artifacts" msgstr "Incluir artefatos de tarefa" -#: src/iac_code/cli/main.py:987 +#: src/iac_code/cli/main.py:1010 msgid "Output format: table or json" msgstr "Formato de saída: table ou json" -#: src/iac_code/cli/main.py:1038 +#: src/iac_code/cli/main.py:1061 msgid "Cancel an A2A task." msgstr "Cancela uma tarefa A2A." -#: src/iac_code/cli/main.py:1088 +#: src/iac_code/cli/main.py:1111 msgid "Subscribe to an A2A task event stream." msgstr "Assina um fluxo de eventos de tarefa A2A." -#: src/iac_code/cli/main.py:1137 +#: src/iac_code/cli/main.py:1160 msgid "Create an A2A task push notification config." msgstr "Cria uma configuração de notificação push de tarefa A2A." -#: src/iac_code/cli/main.py:1142 src/iac_code/cli/main.py:1221 -#: src/iac_code/cli/main.py:1334 +#: src/iac_code/cli/main.py:1165 src/iac_code/cli/main.py:1244 +#: src/iac_code/cli/main.py:1357 msgid "Push config ID" msgstr "ID da configuração push" -#: src/iac_code/cli/main.py:1143 +#: src/iac_code/cli/main.py:1166 msgid "Push callback URL" msgstr "URL de callback push" -#: src/iac_code/cli/main.py:1144 +#: src/iac_code/cli/main.py:1167 msgid "Notification verification token" msgstr "Token de verificação de notificação" -#: src/iac_code/cli/main.py:1145 +#: src/iac_code/cli/main.py:1168 msgid "Callback authentication scheme" msgstr "Esquema de autenticação de callback" -#: src/iac_code/cli/main.py:1146 +#: src/iac_code/cli/main.py:1169 msgid "Callback authentication credentials" msgstr "Credenciais de autenticação de callback" -#: src/iac_code/cli/main.py:1216 +#: src/iac_code/cli/main.py:1239 msgid "Get an A2A task push notification config." msgstr "Obtém uma configuração de notificação push de tarefa A2A." -#: src/iac_code/cli/main.py:1273 +#: src/iac_code/cli/main.py:1296 msgid "List A2A task push notification configs." msgstr "Lista as configurações de notificação push de tarefas A2A." -#: src/iac_code/cli/main.py:1278 +#: src/iac_code/cli/main.py:1301 msgid "Maximum configs to return" msgstr "Número máximo de configurações a retornar" -#: src/iac_code/cli/main.py:1329 +#: src/iac_code/cli/main.py:1352 msgid "Delete an A2A task push notification config." msgstr "Exclui uma configuração de notificação push de tarefa A2A." -#: src/iac_code/cli/main.py:1386 +#: src/iac_code/cli/main.py:1409 msgid "Get an authenticated extended A2A Agent Card." msgstr "Obtém um A2A Agent Card estendido autenticado." -#: src/iac_code/cli/main.py:1429 +#: src/iac_code/cli/main.py:1452 msgid "Preview A2A route resolution." msgstr "Pré-visualização da resolução de rotas A2A." -#: src/iac_code/cli/main.py:1436 +#: src/iac_code/cli/main.py:1459 msgid "Route name to resolve" msgstr "Nome da rota a resolver" -#: src/iac_code/cli/main.py:1437 +#: src/iac_code/cli/main.py:1460 msgid "Skill ID to resolve" msgstr "ID da habilidade a resolver" -#: src/iac_code/cli/main.py:1438 +#: src/iac_code/cli/main.py:1461 msgid "Prompt text used for tag/name route matching" msgstr "Texto do prompt usado para correspondência de rotas por tag/nome" -#: src/iac_code/cli/main.py:1443 +#: src/iac_code/cli/main.py:1466 msgid "Directory for persisted A2A routes" msgstr "Diretório para rotas A2A persistidas" -#: src/iac_code/cli/main.py:1445 +#: src/iac_code/cli/main.py:1468 msgid "Save the provided routes as a route snapshot" msgstr "Salva as rotas fornecidas como um snapshot de rotas" @@ -591,7 +648,7 @@ msgid "[conversation id or search term]" msgstr "[ID da conversa ou termo de busca]" #: src/iac_code/commands/auth.py:382 src/iac_code/commands/auth.py:1107 -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Navigate" msgstr "Navegar" @@ -599,7 +656,7 @@ msgstr "Navegar" #: src/iac_code/commands/auth.py:562 src/iac_code/commands/auth.py:569 #: src/iac_code/commands/auth.py:604 src/iac_code/commands/auth.py:611 #: src/iac_code/commands/auth.py:632 src/iac_code/commands/auth.py:1107 -#: src/iac_code/commands/auth.py:1319 src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/commands/auth.py:1319 src/iac_code/ui/core/prompt_input.py:557 msgid "Confirm" msgstr "Confirmar" @@ -1121,6 +1178,11 @@ msgstr "" "Solução: mude para um provider suportado no QwenPaw, ou desative o modo " "QwenPaw (remova 'llm_source: qwenpaw' do settings.yml)." +#: src/iac_code/services/permissions/loader.py:50 +#, python-brace-format +msgid "Invalid --permission-mode {!r}. Valid values: {}" +msgstr "--permission-mode inválido {!r}. Valores válidos: {}" + #: src/iac_code/services/permissions/pipeline.py:54 #: src/iac_code/tools/base.py:199 src/iac_code/tools/bash/bash_tool.py:175 #, python-brace-format @@ -1847,93 +1909,101 @@ msgstr "Não, sempre negar \"{rule}\" (esta sessão)" msgid "No, always reject this tool" msgstr "Não, sempre rejeitar esta ferramenta" -#: src/iac_code/ui/repl.py:369 +#: src/iac_code/ui/repl.py:370 msgid "Press Ctrl+C again to exit." msgstr "Pressione Ctrl+C novamente para sair." -#: src/iac_code/ui/repl.py:390 +#: src/iac_code/ui/repl.py:395 msgid "Interrupted." msgstr "Interrompido." -#: src/iac_code/ui/repl.py:427 +#: src/iac_code/ui/repl.py:432 msgid "Goodbye!" msgstr "Até logo!" -#: src/iac_code/ui/repl.py:428 +#: src/iac_code/ui/repl.py:433 msgid "Resume this session with:" msgstr "Para retomar esta sessão, execute:" -#: src/iac_code/ui/repl.py:450 +#: src/iac_code/ui/repl.py:458 msgid "Update now" msgstr "Atualizar agora" -#: src/iac_code/ui/repl.py:452 +#: src/iac_code/ui/repl.py:460 msgid "Run the shown update command and exit when it succeeds." msgstr "" "Executa o comando de atualização mostrado e sai quando ele for concluído " "com sucesso." -#: src/iac_code/ui/repl.py:455 +#: src/iac_code/ui/repl.py:463 msgid "Skip" msgstr "Ignorar" -#: src/iac_code/ui/repl.py:457 +#: src/iac_code/ui/repl.py:465 msgid "Continue with the current version for this session." msgstr "Continuar com a versão atual nesta sessão." -#: src/iac_code/ui/repl.py:460 +#: src/iac_code/ui/repl.py:468 msgid "Skip until next version" msgstr "Ignorar até a próxima versão" -#: src/iac_code/ui/repl.py:462 +#: src/iac_code/ui/repl.py:470 msgid "Hide this update until a newer version is available." msgstr "Ocultar esta atualização até que uma versão mais nova esteja disponível." -#: src/iac_code/ui/repl.py:481 src/iac_code/ui/repl.py:493 +#: src/iac_code/ui/repl.py:489 src/iac_code/ui/repl.py:501 msgid "Update command failed. Continuing with the current version." msgstr "O comando de atualização falhou. Continuando com a versão atual." -#: src/iac_code/ui/repl.py:486 +#: src/iac_code/ui/repl.py:494 msgid "Update completed. Restart iac-code to continue." msgstr "Atualização concluída. Reinicie o iac-code para continuar." -#: src/iac_code/ui/repl.py:524 +#: src/iac_code/ui/repl.py:532 msgid "No image in clipboard." msgstr "Nenhuma imagem na área de transferência." -#: src/iac_code/ui/repl.py:677 +#: src/iac_code/ui/repl.py:718 +msgid "Usage: !" +msgstr "Uso: !" + +#: src/iac_code/ui/repl.py:723 +msgid "Shell command support is unavailable." +msgstr "O suporte a comandos shell não está disponível." + +#: src/iac_code/ui/repl.py:787 #, python-brace-format msgid "Unknown skill: ${name}. Type / to list commands and skills." msgstr "" "Habilidade desconhecida: ${name}. Digite / para listar comandos e " "habilidades." -#: src/iac_code/ui/repl.py:679 +#: src/iac_code/ui/repl.py:789 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "Comando desconhecido: /{name}. Digite /help para ver os comandos." -#: src/iac_code/ui/repl.py:684 +#: src/iac_code/ui/repl.py:794 #, python-brace-format msgid "$ only invokes skills. Use /{name} instead." msgstr "$ invoca apenas habilidades. Use /{name} em vez disso." -#: src/iac_code/ui/repl.py:706 src/iac_code/ui/repl.py:751 +#: src/iac_code/ui/repl.py:816 src/iac_code/ui/repl.py:861 #, python-brace-format msgid "Command error: {error}" msgstr "Erro de comando: {error}" -#: src/iac_code/ui/repl.py:713 +#: src/iac_code/ui/repl.py:823 #, python-brace-format msgid "Command has no handler: {name}" msgstr "Comando sem tratador: {name}" -#: src/iac_code/ui/repl.py:1018 +#: src/iac_code/ui/repl.py:1128 #, python-brace-format msgid "Session not found: {session_id}" msgstr "Sessão não encontrada: {session_id}" -#: src/iac_code/ui/repl.py:1037 +#: src/iac_code/ui/repl.py:1147 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1944,19 +2014,19 @@ msgstr "" "Para retomar, execute:\n" " {cmd}" -#: src/iac_code/ui/repl.py:1076 +#: src/iac_code/ui/repl.py:1186 msgid "This conversation is from a different directory." msgstr "Esta conversa é de outro diretório." -#: src/iac_code/ui/repl.py:1078 +#: src/iac_code/ui/repl.py:1188 msgid "To resume, run:" msgstr "Para retomar, execute:" -#: src/iac_code/ui/repl.py:1083 +#: src/iac_code/ui/repl.py:1193 msgid "(Command copied to clipboard)" msgstr "(Comando copiado para a área de transferência)" -#: src/iac_code/ui/repl.py:1240 +#: src/iac_code/ui/repl.py:1350 #, python-brace-format msgid "" "Current model {model} does not support image input. Use /model to switch " @@ -1965,12 +2035,12 @@ msgstr "" "O modelo atual {model} não suporta entrada de imagem. Use /model para " "alternar para um modelo com capacidade de visão." -#: src/iac_code/ui/repl.py:1249 +#: src/iac_code/ui/repl.py:1359 #, python-brace-format msgid "Image error: {err}" msgstr "Erro de imagem: {err}" -#: src/iac_code/ui/repl.py:1266 +#: src/iac_code/ui/repl.py:1376 msgid "" "Failed to persist image to cache; it will only exist in memory for this " "turn." @@ -2010,15 +2080,15 @@ msgstr "Exibindo transcrição · ctrl+o para alternar" msgid "No matches found" msgstr "Nenhuma correspondência" -#: src/iac_code/ui/core/prompt_input.py:337 +#: src/iac_code/ui/core/prompt_input.py:348 msgid "Image in clipboard · ctrl+v to paste" msgstr "Imagem na área de transferência · ctrl+v para colar" -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Fill" msgstr "Preencher" -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Dismiss" msgstr "Dispensar" diff --git a/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po index 7b42f8b..ea8f6f6 100644 --- a/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.3.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-01 16:49+0800\n" +"POT-Creation-Date: 2026-06-01 17:56+0800\n" "PO-Revision-Date: 2026-04-02 00:00+0000\n" "Last-Translator: \n" "Language: zh\n" @@ -17,6 +17,11 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" +#: src/iac_code/config.py:164 +#, python-brace-format +msgid "Invalid IAC_CODE_PROVIDER value: {!r}. Valid values (case-insensitive): {}" +msgstr "无效的 IAC_CODE_PROVIDER 值:{!r}。有效值(不区分大小写):{}" + #: src/iac_code/a2a/transports/base.py:175 msgid "" "Unix domain socket transport is not supported on Windows. Use --transport" @@ -93,7 +98,8 @@ msgstr "调试日志已关闭。" msgid "Usage: /debug [on|off]" msgstr "用法:/debug [on|off]" -#: src/iac_code/agent/agent_loop.py:399 src/iac_code/agent/agent_loop.py:414 +#: src/iac_code/agent/agent_loop.py:404 src/iac_code/agent/agent_loop.py:419 +#: src/iac_code/ui/repl.py:757 src/iac_code/ui/repl.py:771 msgid "Permission denied." msgstr "权限被拒绝。" @@ -115,7 +121,47 @@ msgstr "规划" msgid "Agent" msgstr "智能体" -#: src/iac_code/cli/headless.py:60 +#: src/iac_code/cli/headless.py:50 +#, python-brace-format +msgid "Tool started: {}" +msgstr "工具已开始:{}" + +#: src/iac_code/cli/headless.py:53 +#, python-brace-format +msgid "Tool failed: {}" +msgstr "工具失败:{}" + +#: src/iac_code/cli/headless.py:55 +#, python-brace-format +msgid "Tool finished: {}" +msgstr "工具已完成:{}" + +#: src/iac_code/cli/headless.py:59 +#, python-brace-format +msgid "Child tool failed: {}" +msgstr "子工具失败:{}" + +#: src/iac_code/cli/headless.py:61 +#, python-brace-format +msgid "Child tool finished: {}" +msgstr "子工具已完成:{}" + +#: src/iac_code/cli/headless.py:63 +#, python-brace-format +msgid "Child tool started: {}" +msgstr "子工具已开始:{}" + +#: src/iac_code/cli/headless.py:65 +#, python-brace-format +msgid "Stack {}: {} ({:.1f}%)" +msgstr "资源栈 {}:{} ({:.1f}%)" + +#: src/iac_code/cli/headless.py:71 +#, python-brace-format +msgid "Stack group {}: {} ({}%)" +msgstr "资源栈组 {}:{} ({}%)" + +#: src/iac_code/cli/headless.py:110 #, python-brace-format msgid "" "\n" @@ -179,8 +225,8 @@ msgstr "通过 npmmirror 镜像安装 Git for Windows(仅 Windows)。" msgid "YAML config file containing A2A client options" msgstr "包含 A2A 客户端选项的 YAML 配置文件" -#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1478 -#: src/iac_code/cli/main.py:1839 src/iac_code/cli/main.py:1878 +#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1501 +#: src/iac_code/cli/main.py:1862 src/iac_code/cli/main.py:1901 msgid "" "A2A client dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -202,321 +248,330 @@ msgstr "输出格式:text、json、stream-json" msgid "Maximum agent turns in headless mode" msgstr "无头模式下最大智能体轮次" -#: src/iac_code/cli/main.py:86 src/iac_code/cli/main.py:343 -#: src/iac_code/cli/main.py:568 +#: src/iac_code/cli/main.py:86 src/iac_code/cli/main.py:366 +#: src/iac_code/cli/main.py:591 msgid "Enable debug logging" msgstr "启用调试日志" #: src/iac_code/cli/main.py:87 +msgid "Show headless progress on stderr" +msgstr "在 stderr 显示无头模式进度" + +#: src/iac_code/cli/main.py:88 msgid "Show version and exit" msgstr "显示版本号并退出" -#: src/iac_code/cli/main.py:88 +#: src/iac_code/cli/main.py:89 msgid "Resume a session by ID" msgstr "通过 ID 恢复会话" -#: src/iac_code/cli/main.py:89 +#: src/iac_code/cli/main.py:90 msgid "Resume the most recent session" msgstr "恢复最近一次会话" -#: src/iac_code/cli/main.py:96 src/iac_code/i18n/__init__.py:55 +#: src/iac_code/cli/main.py:97 src/iac_code/i18n/__init__.py:55 msgid "Install completion for the current shell." msgstr "为当前 shell 安装自动补全。" -#: src/iac_code/cli/main.py:104 src/iac_code/i18n/__init__.py:56 +#: src/iac_code/cli/main.py:105 src/iac_code/i18n/__init__.py:56 msgid "" "Show completion for the current shell, to copy it or customize the " "installation." msgstr "显示当前 shell 的自动补全脚本,可复制或自定义安装。" -#: src/iac_code/cli/main.py:109 +#: src/iac_code/cli/main.py:110 msgid "" "Comma-separated tool permission patterns to allow, e.g. 'bash(git " "*),write_file'" msgstr "允许的工具权限模式(逗号分隔),例如 'bash(git *),write_file'" -#: src/iac_code/cli/main.py:114 +#: src/iac_code/cli/main.py:115 msgid "Comma-separated tool permission patterns to deny" msgstr "拒绝的工具权限模式(逗号分隔)" -#: src/iac_code/cli/main.py:119 +#: src/iac_code/cli/main.py:120 msgid "Permission mode: default, accept_edits, bypass_permissions, dont_ask" msgstr "权限模式:default, accept_edits, bypass_permissions, dont_ask" -#: src/iac_code/cli/main.py:150 +#: src/iac_code/cli/main.py:151 msgid "Error: --resume and --continue cannot be used together." msgstr "错误:--resume 和 --continue 不能同时使用。" -#: src/iac_code/cli/main.py:338 +#: src/iac_code/cli/main.py:163 +#, python-brace-format +msgid "Invalid --output-format '{}'. Valid values: {}" +msgstr "无效的 --output-format '{}'。有效值:{}" + +#: src/iac_code/cli/main.py:361 msgid "Run iac-code as an ACP server." msgstr "将 iac-code 作为 ACP 服务器运行。" -#: src/iac_code/cli/main.py:340 +#: src/iac_code/cli/main.py:363 msgid "Transport type: stdio or http" msgstr "传输类型:stdio 或 http" -#: src/iac_code/cli/main.py:341 +#: src/iac_code/cli/main.py:364 msgid "HTTP server port" msgstr "HTTP 服务器端口" -#: src/iac_code/cli/main.py:342 src/iac_code/cli/main.py:558 +#: src/iac_code/cli/main.py:365 src/iac_code/cli/main.py:581 msgid "HTTP server host" msgstr "HTTP 服务器主机" -#: src/iac_code/cli/main.py:554 +#: src/iac_code/cli/main.py:577 msgid "Run iac-code as an A2A 1.0 server." msgstr "将 iac-code 作为 A2A 1.0 服务器运行。" -#: src/iac_code/cli/main.py:557 +#: src/iac_code/cli/main.py:580 msgid "YAML config file for A2A server options" msgstr "用于 A2A 服务器选项的 YAML 配置文件" -#: src/iac_code/cli/main.py:561 +#: src/iac_code/cli/main.py:584 msgid "" "HTTP server port. 41242 is the iac-code default inspired by Gemini CLI, " "not a registered A2A port." msgstr "HTTP 服务器端口。41242 是受 Gemini CLI 启发的 iac-code 默认值,并非已注册的 A2A 端口。" -#: src/iac_code/cli/main.py:566 +#: src/iac_code/cli/main.py:589 msgid "" "A2A transport: http, stdio, unix, websocket, grpc, grpc-jsonrpc, or " "redis-streams" msgstr "A2A 传输方式:http、stdio、unix、websocket、grpc、grpc-jsonrpc 或 redis-streams" -#: src/iac_code/cli/main.py:572 +#: src/iac_code/cli/main.py:595 msgid "" "Expose A2A thinking signal types; repeat for multiple. Values: raw-" "thinking, tool-trace." msgstr "暴露 A2A thinking 信号类型;可重复指定多个。取值:raw-thinking、tool-trace。" -#: src/iac_code/cli/main.py:623 +#: src/iac_code/cli/main.py:646 msgid "" "A2A server dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" msgstr "缺少 A2A 服务器依赖。请使用以下命令安装:pip install 'iac-code[a2a]'" -#: src/iac_code/cli/main.py:755 +#: src/iac_code/cli/main.py:778 msgid "Send a prompt to an A2A JSON-RPC endpoint." msgstr "向 A2A JSON-RPC 端点发送提示。" -#: src/iac_code/cli/main.py:758 src/iac_code/cli/main.py:928 -#: src/iac_code/cli/main.py:981 src/iac_code/cli/main.py:1041 -#: src/iac_code/cli/main.py:1091 src/iac_code/cli/main.py:1140 -#: src/iac_code/cli/main.py:1219 src/iac_code/cli/main.py:1276 -#: src/iac_code/cli/main.py:1332 src/iac_code/cli/main.py:1389 +#: src/iac_code/cli/main.py:781 src/iac_code/cli/main.py:951 +#: src/iac_code/cli/main.py:1004 src/iac_code/cli/main.py:1064 +#: src/iac_code/cli/main.py:1114 src/iac_code/cli/main.py:1163 +#: src/iac_code/cli/main.py:1242 src/iac_code/cli/main.py:1299 +#: src/iac_code/cli/main.py:1355 src/iac_code/cli/main.py:1412 msgid "A2A JSON-RPC endpoint URL" msgstr "A2A JSON-RPC 端点 URL" -#: src/iac_code/cli/main.py:759 src/iac_code/cli/main.py:1434 +#: src/iac_code/cli/main.py:782 src/iac_code/cli/main.py:1457 msgid "Route spec: name=url;skills=skill1,skill2;tags=tag1,tag2" msgstr "路由规范:name=url;skills=skill1,skill2;tags=tag1,tag2" -#: src/iac_code/cli/main.py:760 +#: src/iac_code/cli/main.py:783 msgid "Named A2A route to call" msgstr "要调用的命名 A2A 路由" -#: src/iac_code/cli/main.py:761 +#: src/iac_code/cli/main.py:784 msgid "Prompt to send" msgstr "要发送的提示" -#: src/iac_code/cli/main.py:762 +#: src/iac_code/cli/main.py:785 msgid "Working directory metadata to send with the request" msgstr "随请求一起发送的工作目录元数据" -#: src/iac_code/cli/main.py:763 +#: src/iac_code/cli/main.py:786 msgid "A2A context ID to continue" msgstr "要继续的 A2A 上下文 ID" -#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:859 -#: src/iac_code/cli/main.py:931 src/iac_code/cli/main.py:988 -#: src/iac_code/cli/main.py:1043 src/iac_code/cli/main.py:1093 -#: src/iac_code/cli/main.py:1147 src/iac_code/cli/main.py:1222 -#: src/iac_code/cli/main.py:1280 src/iac_code/cli/main.py:1335 -#: src/iac_code/cli/main.py:1390 +#: src/iac_code/cli/main.py:787 src/iac_code/cli/main.py:882 +#: src/iac_code/cli/main.py:954 src/iac_code/cli/main.py:1011 +#: src/iac_code/cli/main.py:1066 src/iac_code/cli/main.py:1116 +#: src/iac_code/cli/main.py:1170 src/iac_code/cli/main.py:1245 +#: src/iac_code/cli/main.py:1303 src/iac_code/cli/main.py:1358 +#: src/iac_code/cli/main.py:1413 msgid "Bearer token for A2A HTTP requests" msgstr "用于 A2A HTTP 请求的 Bearer 令牌" -#: src/iac_code/cli/main.py:765 src/iac_code/cli/main.py:860 -#: src/iac_code/cli/main.py:932 src/iac_code/cli/main.py:989 -#: src/iac_code/cli/main.py:1044 src/iac_code/cli/main.py:1094 -#: src/iac_code/cli/main.py:1148 src/iac_code/cli/main.py:1223 -#: src/iac_code/cli/main.py:1281 src/iac_code/cli/main.py:1336 -#: src/iac_code/cli/main.py:1391 +#: src/iac_code/cli/main.py:788 src/iac_code/cli/main.py:883 +#: src/iac_code/cli/main.py:955 src/iac_code/cli/main.py:1012 +#: src/iac_code/cli/main.py:1067 src/iac_code/cli/main.py:1117 +#: src/iac_code/cli/main.py:1171 src/iac_code/cli/main.py:1246 +#: src/iac_code/cli/main.py:1304 src/iac_code/cli/main.py:1359 +#: src/iac_code/cli/main.py:1414 msgid "Basic auth username for A2A HTTP requests" msgstr "用于 A2A HTTP 请求的基本认证用户名" -#: src/iac_code/cli/main.py:766 src/iac_code/cli/main.py:861 -#: src/iac_code/cli/main.py:933 src/iac_code/cli/main.py:990 -#: src/iac_code/cli/main.py:1045 src/iac_code/cli/main.py:1095 -#: src/iac_code/cli/main.py:1149 src/iac_code/cli/main.py:1224 -#: src/iac_code/cli/main.py:1282 src/iac_code/cli/main.py:1337 -#: src/iac_code/cli/main.py:1392 +#: src/iac_code/cli/main.py:789 src/iac_code/cli/main.py:884 +#: src/iac_code/cli/main.py:956 src/iac_code/cli/main.py:1013 +#: src/iac_code/cli/main.py:1068 src/iac_code/cli/main.py:1118 +#: src/iac_code/cli/main.py:1172 src/iac_code/cli/main.py:1247 +#: src/iac_code/cli/main.py:1305 src/iac_code/cli/main.py:1360 +#: src/iac_code/cli/main.py:1415 msgid "Basic auth password for A2A HTTP requests" msgstr "用于 A2A HTTP 请求的基本认证密码" -#: src/iac_code/cli/main.py:767 src/iac_code/cli/main.py:862 -#: src/iac_code/cli/main.py:934 src/iac_code/cli/main.py:991 -#: src/iac_code/cli/main.py:1046 src/iac_code/cli/main.py:1096 -#: src/iac_code/cli/main.py:1150 src/iac_code/cli/main.py:1225 -#: src/iac_code/cli/main.py:1283 src/iac_code/cli/main.py:1338 -#: src/iac_code/cli/main.py:1393 +#: src/iac_code/cli/main.py:790 src/iac_code/cli/main.py:885 +#: src/iac_code/cli/main.py:957 src/iac_code/cli/main.py:1014 +#: src/iac_code/cli/main.py:1069 src/iac_code/cli/main.py:1119 +#: src/iac_code/cli/main.py:1173 src/iac_code/cli/main.py:1248 +#: src/iac_code/cli/main.py:1306 src/iac_code/cli/main.py:1361 +#: src/iac_code/cli/main.py:1416 msgid "API key for A2A HTTP requests" msgstr "用于 A2A HTTP 请求的 API 密钥" -#: src/iac_code/cli/main.py:768 src/iac_code/cli/main.py:863 -#: src/iac_code/cli/main.py:935 src/iac_code/cli/main.py:992 -#: src/iac_code/cli/main.py:1047 src/iac_code/cli/main.py:1097 -#: src/iac_code/cli/main.py:1151 src/iac_code/cli/main.py:1226 -#: src/iac_code/cli/main.py:1284 src/iac_code/cli/main.py:1339 -#: src/iac_code/cli/main.py:1394 +#: src/iac_code/cli/main.py:791 src/iac_code/cli/main.py:886 +#: src/iac_code/cli/main.py:958 src/iac_code/cli/main.py:1015 +#: src/iac_code/cli/main.py:1070 src/iac_code/cli/main.py:1120 +#: src/iac_code/cli/main.py:1174 src/iac_code/cli/main.py:1249 +#: src/iac_code/cli/main.py:1307 src/iac_code/cli/main.py:1362 +#: src/iac_code/cli/main.py:1417 msgid "HTTP header name for A2A API key" msgstr "A2A API 密钥使用的 HTTP 请求头名称" -#: src/iac_code/cli/main.py:773 src/iac_code/cli/main.py:868 +#: src/iac_code/cli/main.py:796 src/iac_code/cli/main.py:891 msgid "Secret used to verify the A2A Agent Card" msgstr "用于验证 A2A Agent Card 的密钥" -#: src/iac_code/cli/main.py:778 src/iac_code/cli/main.py:873 +#: src/iac_code/cli/main.py:801 src/iac_code/cli/main.py:896 msgid "Remote JWKS URL used to verify the A2A Agent Card" msgstr "用于验证 A2A Agent Card 的远程 JWKS URL" -#: src/iac_code/cli/main.py:784 src/iac_code/cli/main.py:879 +#: src/iac_code/cli/main.py:807 src/iac_code/cli/main.py:902 msgid "Require a valid A2A Agent Card signature" msgstr "要求 A2A Agent Card 提供有效签名" -#: src/iac_code/cli/main.py:786 +#: src/iac_code/cli/main.py:809 msgid "A2A call timeout in seconds" msgstr "A2A 调用超时时间(秒)" -#: src/iac_code/cli/main.py:787 +#: src/iac_code/cli/main.py:810 msgid "Use A2A streaming message delivery" msgstr "使用 A2A 流式消息传递" -#: src/iac_code/cli/main.py:855 +#: src/iac_code/cli/main.py:878 msgid "Discover an A2A Agent Card." msgstr "发现 A2A Agent Card。" -#: src/iac_code/cli/main.py:858 +#: src/iac_code/cli/main.py:881 msgid "A2A agent base URL" msgstr "A2A 代理基础 URL" -#: src/iac_code/cli/main.py:925 +#: src/iac_code/cli/main.py:948 msgid "Get an A2A task." msgstr "获取 A2A 任务。" -#: src/iac_code/cli/main.py:929 src/iac_code/cli/main.py:1042 -#: src/iac_code/cli/main.py:1092 src/iac_code/cli/main.py:1141 -#: src/iac_code/cli/main.py:1220 src/iac_code/cli/main.py:1277 -#: src/iac_code/cli/main.py:1333 +#: src/iac_code/cli/main.py:952 src/iac_code/cli/main.py:1065 +#: src/iac_code/cli/main.py:1115 src/iac_code/cli/main.py:1164 +#: src/iac_code/cli/main.py:1243 src/iac_code/cli/main.py:1300 +#: src/iac_code/cli/main.py:1356 msgid "A2A task ID" msgstr "A2A 任务 ID" -#: src/iac_code/cli/main.py:930 +#: src/iac_code/cli/main.py:953 msgid "Maximum task history items to return" msgstr "最多返回的任务历史条目数" -#: src/iac_code/cli/main.py:978 +#: src/iac_code/cli/main.py:1001 msgid "List A2A tasks." msgstr "列出 A2A 任务。" -#: src/iac_code/cli/main.py:982 +#: src/iac_code/cli/main.py:1005 msgid "Filter by A2A context ID" msgstr "按 A2A 上下文 ID 过滤" -#: src/iac_code/cli/main.py:983 +#: src/iac_code/cli/main.py:1006 msgid "Filter by A2A task state" msgstr "按 A2A 任务状态过滤" -#: src/iac_code/cli/main.py:984 +#: src/iac_code/cli/main.py:1007 msgid "Maximum tasks to return" msgstr "最多返回的任务数" -#: src/iac_code/cli/main.py:985 src/iac_code/cli/main.py:1279 +#: src/iac_code/cli/main.py:1008 src/iac_code/cli/main.py:1302 msgid "Pagination token" msgstr "分页令牌" -#: src/iac_code/cli/main.py:986 +#: src/iac_code/cli/main.py:1009 msgid "Include task artifacts" msgstr "包含任务工件" -#: src/iac_code/cli/main.py:987 +#: src/iac_code/cli/main.py:1010 msgid "Output format: table or json" msgstr "输出格式:table 或 json" -#: src/iac_code/cli/main.py:1038 +#: src/iac_code/cli/main.py:1061 msgid "Cancel an A2A task." msgstr "取消 A2A 任务。" -#: src/iac_code/cli/main.py:1088 +#: src/iac_code/cli/main.py:1111 msgid "Subscribe to an A2A task event stream." msgstr "订阅 A2A 任务事件流。" -#: src/iac_code/cli/main.py:1137 +#: src/iac_code/cli/main.py:1160 msgid "Create an A2A task push notification config." msgstr "创建 A2A 任务推送通知配置。" -#: src/iac_code/cli/main.py:1142 src/iac_code/cli/main.py:1221 -#: src/iac_code/cli/main.py:1334 +#: src/iac_code/cli/main.py:1165 src/iac_code/cli/main.py:1244 +#: src/iac_code/cli/main.py:1357 msgid "Push config ID" msgstr "推送配置 ID" -#: src/iac_code/cli/main.py:1143 +#: src/iac_code/cli/main.py:1166 msgid "Push callback URL" msgstr "推送回调 URL" -#: src/iac_code/cli/main.py:1144 +#: src/iac_code/cli/main.py:1167 msgid "Notification verification token" msgstr "通知验证令牌" -#: src/iac_code/cli/main.py:1145 +#: src/iac_code/cli/main.py:1168 msgid "Callback authentication scheme" msgstr "回调认证方案" -#: src/iac_code/cli/main.py:1146 +#: src/iac_code/cli/main.py:1169 msgid "Callback authentication credentials" msgstr "回调认证凭据" -#: src/iac_code/cli/main.py:1216 +#: src/iac_code/cli/main.py:1239 msgid "Get an A2A task push notification config." msgstr "获取 A2A 任务推送通知配置。" -#: src/iac_code/cli/main.py:1273 +#: src/iac_code/cli/main.py:1296 msgid "List A2A task push notification configs." msgstr "列出 A2A 任务推送通知配置。" -#: src/iac_code/cli/main.py:1278 +#: src/iac_code/cli/main.py:1301 msgid "Maximum configs to return" msgstr "最多返回的配置数" -#: src/iac_code/cli/main.py:1329 +#: src/iac_code/cli/main.py:1352 msgid "Delete an A2A task push notification config." msgstr "删除 A2A 任务推送通知配置。" -#: src/iac_code/cli/main.py:1386 +#: src/iac_code/cli/main.py:1409 msgid "Get an authenticated extended A2A Agent Card." msgstr "获取经过身份验证的扩展 A2A Agent Card。" -#: src/iac_code/cli/main.py:1429 +#: src/iac_code/cli/main.py:1452 msgid "Preview A2A route resolution." msgstr "预览 A2A 路由解析。" -#: src/iac_code/cli/main.py:1436 +#: src/iac_code/cli/main.py:1459 msgid "Route name to resolve" msgstr "要解析的路由名称" -#: src/iac_code/cli/main.py:1437 +#: src/iac_code/cli/main.py:1460 msgid "Skill ID to resolve" msgstr "要解析的技能 ID" -#: src/iac_code/cli/main.py:1438 +#: src/iac_code/cli/main.py:1461 msgid "Prompt text used for tag/name route matching" msgstr "用于标签/名称路由匹配的提示文本" -#: src/iac_code/cli/main.py:1443 +#: src/iac_code/cli/main.py:1466 msgid "Directory for persisted A2A routes" msgstr "持久化 A2A 路由的目录" -#: src/iac_code/cli/main.py:1445 +#: src/iac_code/cli/main.py:1468 msgid "Save the provided routes as a route snapshot" msgstr "将提供的路由保存为路由快照" @@ -565,7 +620,7 @@ msgid "[conversation id or search term]" msgstr "[会话 ID 或搜索词]" #: src/iac_code/commands/auth.py:382 src/iac_code/commands/auth.py:1107 -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Navigate" msgstr "导航" @@ -573,7 +628,7 @@ msgstr "导航" #: src/iac_code/commands/auth.py:562 src/iac_code/commands/auth.py:569 #: src/iac_code/commands/auth.py:604 src/iac_code/commands/auth.py:611 #: src/iac_code/commands/auth.py:632 src/iac_code/commands/auth.py:1107 -#: src/iac_code/commands/auth.py:1319 src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/commands/auth.py:1319 src/iac_code/ui/core/prompt_input.py:557 msgid "Confirm" msgstr "确认" @@ -1084,6 +1139,11 @@ msgstr "" "解决方法:在 QwenPaw 中切换到已支持的 provider,或关闭 QwenPaw 模式(从 settings.yml 移除 " "'llm_source: qwenpaw')。" +#: src/iac_code/services/permissions/loader.py:50 +#, python-brace-format +msgid "Invalid --permission-mode {!r}. Valid values: {}" +msgstr "无效的 --permission-mode {!r}。有效值:{}" + #: src/iac_code/services/permissions/pipeline.py:54 #: src/iac_code/tools/base.py:199 src/iac_code/tools/bash/bash_tool.py:175 #, python-brace-format @@ -1794,89 +1854,97 @@ msgstr "否,始终拒绝 \"{rule}\"(本次会话)" msgid "No, always reject this tool" msgstr "否,始终拒绝此工具" -#: src/iac_code/ui/repl.py:369 +#: src/iac_code/ui/repl.py:370 msgid "Press Ctrl+C again to exit." msgstr "再次按 Ctrl+C 退出。" -#: src/iac_code/ui/repl.py:390 +#: src/iac_code/ui/repl.py:395 msgid "Interrupted." msgstr "已中断。" -#: src/iac_code/ui/repl.py:427 +#: src/iac_code/ui/repl.py:432 msgid "Goodbye!" msgstr "再见!" -#: src/iac_code/ui/repl.py:428 +#: src/iac_code/ui/repl.py:433 msgid "Resume this session with:" msgstr "恢复此会话请运行:" -#: src/iac_code/ui/repl.py:450 +#: src/iac_code/ui/repl.py:458 msgid "Update now" msgstr "立即更新" -#: src/iac_code/ui/repl.py:452 +#: src/iac_code/ui/repl.py:460 msgid "Run the shown update command and exit when it succeeds." msgstr "运行显示的更新命令,成功后退出。" -#: src/iac_code/ui/repl.py:455 +#: src/iac_code/ui/repl.py:463 msgid "Skip" msgstr "跳过" -#: src/iac_code/ui/repl.py:457 +#: src/iac_code/ui/repl.py:465 msgid "Continue with the current version for this session." msgstr "本次会话继续使用当前版本。" -#: src/iac_code/ui/repl.py:460 +#: src/iac_code/ui/repl.py:468 msgid "Skip until next version" msgstr "跳过直到下一个版本" -#: src/iac_code/ui/repl.py:462 +#: src/iac_code/ui/repl.py:470 msgid "Hide this update until a newer version is available." msgstr "隐藏此更新,直到有更新的版本可用。" -#: src/iac_code/ui/repl.py:481 src/iac_code/ui/repl.py:493 +#: src/iac_code/ui/repl.py:489 src/iac_code/ui/repl.py:501 msgid "Update command failed. Continuing with the current version." msgstr "更新命令失败。将继续使用当前版本。" -#: src/iac_code/ui/repl.py:486 +#: src/iac_code/ui/repl.py:494 msgid "Update completed. Restart iac-code to continue." msgstr "更新已完成。请重启 iac-code 以继续。" -#: src/iac_code/ui/repl.py:524 +#: src/iac_code/ui/repl.py:532 msgid "No image in clipboard." msgstr "剪贴板中没有图像。" -#: src/iac_code/ui/repl.py:677 +#: src/iac_code/ui/repl.py:718 +msgid "Usage: !" +msgstr "用法:!" + +#: src/iac_code/ui/repl.py:723 +msgid "Shell command support is unavailable." +msgstr "Shell 命令支持不可用。" + +#: src/iac_code/ui/repl.py:787 #, python-brace-format msgid "Unknown skill: ${name}. Type / to list commands and skills." msgstr "未知技能:${name}。输入 / 可列出命令和技能。" -#: src/iac_code/ui/repl.py:679 +#: src/iac_code/ui/repl.py:789 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "未知命令:/{name}。输入 /help 查看可用命令。" -#: src/iac_code/ui/repl.py:684 +#: src/iac_code/ui/repl.py:794 #, python-brace-format msgid "$ only invokes skills. Use /{name} instead." msgstr "$ 只能调用技能。请改用 /{name}。" -#: src/iac_code/ui/repl.py:706 src/iac_code/ui/repl.py:751 +#: src/iac_code/ui/repl.py:816 src/iac_code/ui/repl.py:861 #, python-brace-format msgid "Command error: {error}" msgstr "命令错误:{error}" -#: src/iac_code/ui/repl.py:713 +#: src/iac_code/ui/repl.py:823 #, python-brace-format msgid "Command has no handler: {name}" msgstr "命令没有处理器:{name}" -#: src/iac_code/ui/repl.py:1018 +#: src/iac_code/ui/repl.py:1128 #, python-brace-format msgid "Session not found: {session_id}" msgstr "会话不存在:{session_id}" -#: src/iac_code/ui/repl.py:1037 +#: src/iac_code/ui/repl.py:1147 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1887,31 +1955,31 @@ msgstr "" "请运行以下命令恢复:\n" " {cmd}" -#: src/iac_code/ui/repl.py:1076 +#: src/iac_code/ui/repl.py:1186 msgid "This conversation is from a different directory." msgstr "该会话来自另一个目录。" -#: src/iac_code/ui/repl.py:1078 +#: src/iac_code/ui/repl.py:1188 msgid "To resume, run:" msgstr "请运行以下命令恢复:" -#: src/iac_code/ui/repl.py:1083 +#: src/iac_code/ui/repl.py:1193 msgid "(Command copied to clipboard)" msgstr "(命令已复制到剪贴板)" -#: src/iac_code/ui/repl.py:1240 +#: src/iac_code/ui/repl.py:1350 #, python-brace-format msgid "" "Current model {model} does not support image input. Use /model to switch " "to a vision-capable model." msgstr "当前模型 {model} 不支持图像输入。请使用 /model 切换到支持视觉的模型。" -#: src/iac_code/ui/repl.py:1249 +#: src/iac_code/ui/repl.py:1359 #, python-brace-format msgid "Image error: {err}" msgstr "图像错误:{err}" -#: src/iac_code/ui/repl.py:1266 +#: src/iac_code/ui/repl.py:1376 msgid "" "Failed to persist image to cache; it will only exist in memory for this " "turn." @@ -1949,15 +2017,15 @@ msgstr "显示完整记录 · 按 ctrl+o 切换" msgid "No matches found" msgstr "未找到匹配项" -#: src/iac_code/ui/core/prompt_input.py:337 +#: src/iac_code/ui/core/prompt_input.py:348 msgid "Image in clipboard · ctrl+v to paste" msgstr "剪贴板中有图像 · 按 ctrl+v 粘贴" -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Fill" msgstr "填充" -#: src/iac_code/ui/core/prompt_input.py:546 +#: src/iac_code/ui/core/prompt_input.py:557 msgid "Dismiss" msgstr "关闭" diff --git a/src/iac_code/memory/memory_manager.py b/src/iac_code/memory/memory_manager.py index 74282b5..bfe0369 100644 --- a/src/iac_code/memory/memory_manager.py +++ b/src/iac_code/memory/memory_manager.py @@ -3,20 +3,40 @@ from __future__ import annotations import os +import re +from pathlib import Path from typing import Any +from iac_code.utils.file_security import ensure_private_dir, ensure_private_file + MEMORY_TYPES = {"user", "feedback", "project", "reference"} INDEX_FILE = "MEMORY.md" MAX_INDEX_LINES = 200 +_MEMORY_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]+$") +_RESERVED_MEMORY_FILENAMES = {INDEX_FILE.casefold()} class MemoryManager: def __init__(self, memory_dir: str): self._memory_dir = memory_dir - os.makedirs(memory_dir, exist_ok=True) + ensure_private_dir(Path(memory_dir)) + + @staticmethod + def _validate_name(name: str) -> str: + cleaned = name.strip() + if not cleaned or cleaned in {".", ".."}: + raise ValueError(f"Invalid memory name: {name!r}") + if "/" in cleaned or "\\" in cleaned or ".." in cleaned: + raise ValueError(f"Invalid memory name: {name!r}") + if os.path.isabs(cleaned) or not _MEMORY_NAME_RE.fullmatch(cleaned): + raise ValueError(f"Invalid memory name: {name!r}") + if f"{cleaned}.md".casefold() in _RESERVED_MEMORY_FILENAMES: + raise ValueError(f"Invalid memory name: {name!r}") + return cleaned def _memory_path(self, name: str) -> str: - return os.path.join(self._memory_dir, f"{name}.md") + safe_name = self._validate_name(name) + return os.path.join(self._memory_dir, f"{safe_name}.md") def _index_path(self) -> str: return os.path.join(self._memory_dir, INDEX_FILE) @@ -25,16 +45,17 @@ def save(self, name: str, content: str, memory_type: str, description: str) -> N if memory_type not in MEMORY_TYPES: raise ValueError(f"Invalid memory type: {memory_type}") file_content = f"---\nname: {name}\ndescription: {description}\ntype: {memory_type}\n---\n\n{content}\n" - with open(self._memory_path(name), "w", encoding="utf-8", newline="\n") as f: + path = self._memory_path(name) + with open(path, "w", encoding="utf-8", newline="\n") as f: f.write(file_content) + ensure_private_file(Path(path)) self._update_index() def load(self, name: str) -> dict[str, Any] | None: path = self._memory_path(name) if not os.path.exists(path): return None - with open(path, encoding="utf-8") as f: - return self._parse_memory_file(f.read()) + return self._load_memory_file(Path(path)) def delete(self, name: str) -> None: path = self._memory_path(name) @@ -44,11 +65,10 @@ def delete(self, name: str) -> None: def list_memories(self) -> list[dict[str, Any]]: memories = [] - for filename in os.listdir(self._memory_dir): - if filename.endswith(".md") and filename != INDEX_FILE: - mem = self.load(filename[:-3]) - if mem: - memories.append(mem) + for path in self._iter_memory_files(): + mem = self._load_memory_file(path) + if mem: + memories.append(mem) return memories def get_index_content(self) -> str: @@ -66,13 +86,28 @@ def get_prompt_content(self) -> str: def _update_index(self) -> None: entries = [] - for filename in sorted(os.listdir(self._memory_dir)): - if filename.endswith(".md") and filename != INDEX_FILE: - mem = self.load(filename[:-3]) - if mem: - entries.append(f"- [{filename[:-3]}]({filename}) — {mem.get('description', '')}") - with open(self._index_path(), "w", encoding="utf-8", newline="\n") as f: + for path in sorted(self._iter_memory_files(), key=lambda item: item.name): + mem = self._load_memory_file(path) + if mem: + entries.append(f"- [{path.stem}]({path.name}) — {mem.get('description', '')}") + index_path = self._index_path() + with open(index_path, "w", encoding="utf-8", newline="\n") as f: f.write("\n".join(entries[:MAX_INDEX_LINES]) + "\n") + ensure_private_file(Path(index_path)) + + def _iter_memory_files(self) -> list[Path]: + root = Path(self._memory_dir) + return [ + path + for path in root.iterdir() + if path.is_file() and path.suffix == ".md" and path.name.casefold() != INDEX_FILE.casefold() + ] + + def _load_memory_file(self, path: Path) -> dict[str, Any] | None: + try: + return self._parse_memory_file(path.read_text(encoding="utf-8")) + except OSError: + return None @staticmethod def _parse_memory_file(text: str) -> dict[str, Any]: diff --git a/src/iac_code/services/agent_factory.py b/src/iac_code/services/agent_factory.py index 4f1a26a..dbbbbee 100644 --- a/src/iac_code/services/agent_factory.py +++ b/src/iac_code/services/agent_factory.py @@ -167,6 +167,7 @@ def create_agent_runtime(options: AgentFactoryOptions) -> AgentRuntime: max_turns=options.max_turns, cwd=cwd, permission_context=permission_context, + auto_trigger_skills=command_registry.get_model_invocable_skills(), ) return AgentRuntime( diff --git a/src/iac_code/services/permissions/loader.py b/src/iac_code/services/permissions/loader.py index c5de860..190e4eb 100644 --- a/src/iac_code/services/permissions/loader.py +++ b/src/iac_code/services/permissions/loader.py @@ -8,6 +8,7 @@ import yaml from loguru import logger +from iac_code.i18n import _ from iac_code.types.permissions import PermissionMode, ToolPermissionContext @@ -40,6 +41,15 @@ def _coerce_str_list(value: Any) -> list[str]: return out +def parse_cli_permission_mode(value: str) -> PermissionMode: + """Parse a CLI permission mode, raising on invalid explicit input.""" + try: + return PermissionMode(value) + except ValueError as exc: + valid = ", ".join(m.value for m in PermissionMode) + raise ValueError(_("Invalid --permission-mode {!r}. Valid values: {}").format(value, valid)) from exc + + def load_settings_permissions(path: Path, source: str) -> dict[str, Any]: """Load the permissions section from a single settings.yml file. @@ -147,11 +157,7 @@ def load_permission_context( if cli_disallowed: deny_rules["cli_arg"] = list(cli_disallowed) if cli_mode is not None: - try: - mode_holder[0] = PermissionMode(cli_mode) - except ValueError: - valid = ", ".join(m.value for m in PermissionMode) - logger.warning("Invalid --permission-mode '{}'; valid: {}", cli_mode, valid) + mode_holder[0] = parse_cli_permission_mode(cli_mode) resolved_mode = mode_holder[0] if mode_holder[0] is not None else PermissionMode.DEFAULT diff --git a/src/iac_code/services/session_storage.py b/src/iac_code/services/session_storage.py index b3a99a8..380525f 100644 --- a/src/iac_code/services/session_storage.py +++ b/src/iac_code/services/session_storage.py @@ -24,6 +24,7 @@ from iac_code import __version__ from iac_code.agent.message import ContentBlock, Message, ToolResultBlock +from iac_code.utils.file_security import ensure_private_dir, ensure_private_file from iac_code.utils.project_paths import ( get_project_dir, get_projects_dir, @@ -35,7 +36,7 @@ class SessionStorage: """Persist conversation sessions partitioned by working directory.""" def __init__(self, projects_dir: Path | str | None = None) -> None: - self._projects_dir = Path(projects_dir) if projects_dir is not None else get_projects_dir() + self._projects_dir = ensure_private_dir(Path(projects_dir) if projects_dir is not None else get_projects_dir()) # ------------------------------------------------------------------ # Internal path helpers @@ -86,21 +87,23 @@ def append( ) -> None: """Append a single message (real-time persistence).""" path = self._session_path(cwd, session_id) - path.parent.mkdir(parents=True, exist_ok=True) + ensure_private_dir(path.parent) data = self._stamp(message.to_dict(), cwd, session_id, git_branch) with open(path, "a", encoding="utf-8") as f: f.write(json.dumps(data, ensure_ascii=False) + "\n") + ensure_private_file(path) def append_meta(self, cwd: str, session_id: str, meta_entry: dict[str, Any]) -> None: """Append a lite-meta row (no ``role``, distinguished by ``type``).""" if "type" not in meta_entry: raise ValueError("meta_entry must include a 'type' field") path = self._session_path(cwd, session_id) - path.parent.mkdir(parents=True, exist_ok=True) + ensure_private_dir(path.parent) entry = dict(meta_entry) entry["session_id"] = session_id with open(path, "a", encoding="utf-8") as f: f.write(json.dumps(entry, ensure_ascii=False) + "\n") + ensure_private_file(path) def save( self, @@ -112,11 +115,12 @@ def save( ) -> None: """Overwrite the session file with the given messages.""" path = self._session_path(cwd, session_id) - path.parent.mkdir(parents=True, exist_ok=True) + ensure_private_dir(path.parent) with open(path, "w", encoding="utf-8") as f: for msg in messages: data = self._stamp(msg.to_dict(), cwd, session_id, git_branch) f.write(json.dumps(data, ensure_ascii=False) + "\n") + ensure_private_file(path) # ------------------------------------------------------------------ # Read diff --git a/src/iac_code/services/telemetry/fallback.py b/src/iac_code/services/telemetry/fallback.py index 7d658bd..181572f 100644 --- a/src/iac_code/services/telemetry/fallback.py +++ b/src/iac_code/services/telemetry/fallback.py @@ -7,6 +7,8 @@ from collections.abc import Iterable, Iterator from pathlib import Path +from iac_code.utils.file_security import ensure_private_dir, ensure_private_file + _FAILED_PREFIX = "failed_events." _FAILED_SUFFIX = ".jsonl" @@ -18,8 +20,7 @@ def __init__(self, base_dir: Path) -> None: self._base_dir = base_dir def _ensure_dir(self) -> Path: - self._base_dir.mkdir(parents=True, exist_ok=True) - return self._base_dir + return ensure_private_dir(self._base_dir) def write(self, session_id: str, events: Iterable[dict]) -> Path: """Write one JSONL file per batch. One line per event.""" @@ -28,6 +29,7 @@ def write(self, session_id: str, events: Iterable[dict]) -> Path: with path.open("w", encoding="utf-8") as fh: for event in events: fh.write(json.dumps(event, ensure_ascii=False) + "\n") + ensure_private_file(path) return path def list_pending(self) -> Iterator[Path]: diff --git a/src/iac_code/skills/auto_trigger.py b/src/iac_code/skills/auto_trigger.py new file mode 100644 index 0000000..ecbe981 --- /dev/null +++ b/src/iac_code/skills/auto_trigger.py @@ -0,0 +1,115 @@ +"""Generic skill auto-trigger dispatcher.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path +from types import ModuleType +from typing import Any + +from loguru import logger + +from iac_code.commands.registry import PromptCommand +from iac_code.skills.processor import ProcessedSkillResult, process_prompt_command +from iac_code.types.skill_source import SkillSource + + +def has_skill_tag(content: str, skill_name: str) -> bool: + return f"{skill_name}" in content + + +def context_has_skill_tag(messages: list[Any], skill_name: str) -> bool: + for message in messages: + content = message.get("content", "") if isinstance(message, dict) else getattr(message, "content", "") + if isinstance(content, str) and has_skill_tag(content, skill_name): + return True + if isinstance(content, list): + for block in content: + if isinstance(block, dict): + text = block.get("text") or block.get("content") + else: + text = getattr(block, "text", None) or getattr(block, "content", None) + if isinstance(text, str) and has_skill_tag(text, skill_name): + return True + return False + + +def find_auto_triggered_skills( + prompt: str, + skills: list[PromptCommand], + *, + loaded_skill_names: set[str], + context_messages: list[Any] | None = None, +) -> list[PromptCommand]: + if not prompt.strip(): + return [] + + matches: list[PromptCommand] = [] + context_messages = context_messages or [] + for command in skills: + skill = command.skill + if skill is None or command.name in loaded_skill_names: + continue + if context_has_skill_tag(context_messages, command.name): + loaded_skill_names.add(command.name) + continue + script = skill.auto_trigger.get("script") + if not script or command.source != SkillSource.BUNDLED or skill.source != SkillSource.BUNDLED: + continue + module = _load_trigger_module(skill.skill_root, script, command.name) + if module is None or not getattr(module, "ENABLE_AUTO_TRIGGER", True): + continue + should_trigger = getattr(module, "should_trigger", None) + if not callable(should_trigger): + continue + try: + if should_trigger(prompt): + matches.append(command) + except Exception as exc: + logger.warning("Skill auto-trigger failed for {}: {}", command.name, exc) + return matches + + +async def process_auto_triggered_skills( + prompt: str, + skills: list[PromptCommand], + *, + loaded_skill_names: set[str], + context_messages: list[Any] | None = None, + session_id: str = "", +) -> list[ProcessedSkillResult]: + results: list[ProcessedSkillResult] = [] + for command in find_auto_triggered_skills( + prompt, + skills, + loaded_skill_names=loaded_skill_names, + context_messages=context_messages, + ): + result = await process_prompt_command(command, "", session_id=session_id) + loaded_skill_names.add(command.name) + results.append(result) + return results + + +def _load_trigger_module(skill_root: str, script: str, skill_name: str) -> ModuleType | None: + if not skill_root: + return None + script_path = (Path(skill_root) / script).resolve() + root_path = Path(skill_root).resolve() + if root_path not in script_path.parents: + logger.warning("Skill auto-trigger script escapes skill root: {}", script) + return None + if not script_path.is_file(): + logger.warning("Skill auto-trigger script not found for {}: {}", skill_name, script_path) + return None + module_name = f"iac_code_skill_auto_trigger_{skill_name.replace('-', '_')}_{abs(hash(str(script_path)))}" + spec = importlib.util.spec_from_file_location(module_name, script_path) + if spec is None or spec.loader is None: + return None + module = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(module) + except Exception as exc: + logger.warning("Failed to load skill auto-trigger script for {}: {}", skill_name, exc) + return None + return module diff --git a/src/iac_code/skills/bundled/__init__.py b/src/iac_code/skills/bundled/__init__.py index 842728c..d17678a 100644 --- a/src/iac_code/skills/bundled/__init__.py +++ b/src/iac_code/skills/bundled/__init__.py @@ -28,6 +28,7 @@ def register_bundled_skill( context: str = "inline", agent: str = "general-purpose", skill_root: str = "", + auto_trigger: dict[str, str] | None = None, ) -> None: """Register a bundled skill.""" frontmatter = SkillFrontmatter( @@ -42,6 +43,7 @@ def register_bundled_skill( user_invocable=user_invocable, context=context, agent=agent, + auto_trigger=auto_trigger or {}, ) # Create prompt provider diff --git a/src/iac_code/skills/bundled/iac_aliyun/SKILL.md b/src/iac_code/skills/bundled/iac_aliyun/SKILL.md index a14f9f8..adf5120 100644 --- a/src/iac_code/skills/bundled/iac_aliyun/SKILL.md +++ b/src/iac_code/skills/bundled/iac_aliyun/SKILL.md @@ -1,8 +1,10 @@ --- name: iac-aliyun -description: 阿里云 IaC 模板生成、解释、完善与部署 -when_to_use: 当用户涉及云资源创建、模板生成、模板解释、部署等 IaC 相关操作时 +description: 阿里云 Alibaba Cloud ROS/Terraform IaC 模板生成、解释、完善、校验、询价与部署 +when_to_use: 当用户请求阿里云/Alibaba Cloud/Alicloud 的 ROS 模板、资源栈、Terraform alicloud provider 模板生成、解释、完善、校验、询价、部署、更新或删除时,必须先调用 skill 工具加载 iac-aliyun。 user_invocable: false +auto_trigger: + script: auto_trigger.py --- # 阿里云 IaC 技能 diff --git a/src/iac_code/skills/bundled/iac_aliyun/__init__.py b/src/iac_code/skills/bundled/iac_aliyun/__init__.py index a66ebfa..69752f4 100644 --- a/src/iac_code/skills/bundled/iac_aliyun/__init__.py +++ b/src/iac_code/skills/bundled/iac_aliyun/__init__.py @@ -8,9 +8,13 @@ def register_iac_aliyun_skill() -> None: register_bundled_skill( name="iac-aliyun", - description="阿里云 IaC 模板生成、解释、完善与部署", + description="阿里云 Alibaba Cloud ROS/Terraform IaC 模板生成、解释、完善、校验、询价与部署", prompt=(SKILL_DIR / "SKILL.md").read_text(encoding="utf-8"), - when_to_use="当用户涉及云资源创建、模板生成、模板解释、部署等 IaC 相关操作时", + when_to_use=( + "当用户请求阿里云/Alibaba Cloud/Alicloud 的 ROS 模板、资源栈、Terraform alicloud provider " + "模板生成、解释、完善、校验、询价、部署、更新或删除时,必须先调用 skill 工具加载 iac-aliyun。" + ), user_invocable=False, skill_root=str(SKILL_DIR), + auto_trigger={"script": "auto_trigger.py"}, ) diff --git a/src/iac_code/skills/bundled/iac_aliyun/auto_trigger.py b/src/iac_code/skills/bundled/iac_aliyun/auto_trigger.py new file mode 100644 index 0000000..a5cffba --- /dev/null +++ b/src/iac_code/skills/bundled/iac_aliyun/auto_trigger.py @@ -0,0 +1,88 @@ +"""Auto-trigger rules for the bundled iac-aliyun skill.""" + +from __future__ import annotations + +import re + +ENABLE_AUTO_TRIGGER = True + +_ALIYUN_SCOPE_PATTERNS = [ + r"阿里云", + r"\baliyun\b", + r"\balicloud\b", + r"\balibaba\s+cloud\b", + r"资源编排", + r"\bresource\s+orchestration\s+service\b", + r"rostemplateformatversion", + r"aliyun::", + r"datasource::", + r"alicloud\s+provider", + r'provider\s+"alicloud"', + r'resource\s+"alicloud_', +] + +_ES_TEMPLATE_ACTIONS = r"genera|generar|crea|crear|despliega|desplegar|explica|explicar|valida|validar|mejora|mejorar" +_FR_TEMPLATE_ACTIONS = ( + r"cr[eé]e|cr[eé]er|g[eé]n[eé]re|g[eé]n[eé]rer|d[eé]ploie|d[eé]ployer|" + r"explique|expliquer|valide|valider|am[eé]liore|am[eé]liorer" +) +_DE_TEMPLATE_ACTIONS = ( + r"erstelle|erstellen|generiere|generieren|bereitstelle|bereitstellen|" + r"erkl[aä]re|erkl[aä]ren|validiere|validieren|verbessere|verbessern" +) +_PT_TEMPLATE_ACTIONS = r"gere|gerar|crie|criar|implante|implantar|explique|explicar|valide|validar|melhore|melhorar" +_ZH_IAC_NOUNS = r"模[板版]|资源栈|\bros\b|\bterraform\b" + +_IAC_WORKFLOW_PATTERNS = [ + r"\bterraform\b", + r"\bros[-\s]+template\b", + r"\b(create|generate|write|deploy|explain|validate|improve|update|delete)\b.*\b(template|stack)\b", + r"\b(template|stack)\b.*\b(create|generate|write|deploy|explain|validate|improve|update|delete)\b", + r"ros\s*模[板版]", + r"模板生成", + r"模版生成", + r"生成.*模[板版]", + r"编写.*模[板版]", + r"写.*模[板版]", + r"解释.*模[板版]", + r"完善.*模[板版]", + r"校验.*模[板版]", + r"验证.*模[板版]", + r"更新.*模[板版]", + r"删除.*模[板版]", + r"资源栈", + rf"部署.*({_ZH_IAC_NOUNS})", + rf"({_ZH_IAC_NOUNS}).*部署", + rf"({_ES_TEMPLATE_ACTIONS}).*plantilla", + rf"plantilla.*({_ES_TEMPLATE_ACTIONS})", + r"plantilla\s+ros", + rf"({_FR_TEMPLATE_ACTIONS}).*mod[eè]le", + rf"mod[eè]le.*({_FR_TEMPLATE_ACTIONS})", + r"mod[eè]le\s+ros", + rf"({_DE_TEMPLATE_ACTIONS}).*vorlage", + rf"vorlage.*({_DE_TEMPLATE_ACTIONS})", + r"ros[-\s]*vorlage", + r"(生成|作成|デプロイ|説明|検証|改善|更新|削除).*テンプレート", + r"テンプレート.*(生成|作成|デプロイ|説明|検証|改善|更新|削除)", + r"ros\s*テンプレート", + rf"({_PT_TEMPLATE_ACTIONS}).*modelo", + rf"modelo.*({_PT_TEMPLATE_ACTIONS})", + r"modelo\s+ros", + r"\bcreatestack\b", + r"\bvalidatetemplate\b", + r"\.tf\b", + r"\.ros\.ya?ml\b", +] + + +def should_trigger(prompt: str) -> bool: + text = prompt.casefold() + return has_aliyun_scope(text) and has_iac_workflow(text) + + +def has_aliyun_scope(text: str) -> bool: + return any(re.search(pattern, text, re.IGNORECASE) for pattern in _ALIYUN_SCOPE_PATTERNS) + + +def has_iac_workflow(text: str) -> bool: + return any(re.search(pattern, text, re.IGNORECASE) for pattern in _IAC_WORKFLOW_PATTERNS) diff --git a/src/iac_code/skills/frontmatter.py b/src/iac_code/skills/frontmatter.py index 587c5bd..4ba2869 100644 --- a/src/iac_code/skills/frontmatter.py +++ b/src/iac_code/skills/frontmatter.py @@ -31,6 +31,7 @@ class SkillFrontmatter: context: str = "inline" # "inline" | "fork" agent: str = "general-purpose" paths: list[str] = field(default_factory=list) + auto_trigger: dict[str, str] = field(default_factory=dict) def parse_frontmatter(markdown: str) -> tuple[SkillFrontmatter, str]: @@ -115,5 +116,8 @@ def _data_to_frontmatter(data: dict[str, Any]) -> SkillFrontmatter: fm.context = data.get("context", "inline") fm.agent = data.get("agent", "general-purpose") fm.paths = list(data.get("paths", [])) + auto_trigger = data.get("auto_trigger", {}) + if isinstance(auto_trigger, dict): + fm.auto_trigger = {str(k): str(v) for k, v in auto_trigger.items() if v is not None} return fm diff --git a/src/iac_code/skills/skill_definition.py b/src/iac_code/skills/skill_definition.py index 144af4d..8a494a7 100644 --- a/src/iac_code/skills/skill_definition.py +++ b/src/iac_code/skills/skill_definition.py @@ -72,6 +72,10 @@ def agent_type(self) -> str: def when_to_use(self) -> str: return self.frontmatter.when_to_use + @property + def auto_trigger(self) -> dict[str, str]: + return self.frontmatter.auto_trigger + async def get_prompt(self, args: str, context: SkillContext) -> str: """Generate the final prompt content.""" if self._prompt_provider is not None: diff --git a/src/iac_code/tools/result_storage.py b/src/iac_code/tools/result_storage.py index af4b5bd..75ec460 100644 --- a/src/iac_code/tools/result_storage.py +++ b/src/iac_code/tools/result_storage.py @@ -2,11 +2,32 @@ from __future__ import annotations -import os +import re from dataclasses import dataclass +from hashlib import blake2b +from pathlib import Path + +from iac_code.utils.file_security import ensure_private_dir, ensure_private_file DEFAULT_MAX_INLINE_CHARS = 50_000 DEFAULT_PREVIEW_CHARS = 2_000 +_SAFE_TOOL_USE_ID_RE = re.compile(r"^[A-Za-z0-9_.-]+$") + + +def _result_filename(tool_use_id: str) -> str: + cleaned = tool_use_id.strip() + if ( + cleaned + and cleaned not in {".", ".."} + and "/" not in cleaned + and "\\" not in cleaned + and ".." not in cleaned + and not Path(cleaned).is_absolute() + and _SAFE_TOOL_USE_ID_RE.fullmatch(cleaned) + ): + return f"{cleaned}.txt" + digest = blake2b(tool_use_id.encode("utf-8"), digest_size=12).hexdigest() + return f"tool_result_{digest}.txt" @dataclass @@ -30,10 +51,14 @@ def __init__( def process(self, tool_use_id: str, content: str) -> ProcessedResult: if len(content) <= self._max_inline_chars: return ProcessedResult(content=content) - os.makedirs(self._storage_dir, exist_ok=True) - file_path = os.path.join(self._storage_dir, f"{tool_use_id}.txt") - with open(file_path, "w", encoding="utf-8") as f: + storage_path = Path(self._storage_dir) + if storage_path.parent.name == "tool-results": + ensure_private_dir(storage_path.parent) + storage_dir = ensure_private_dir(storage_path) + file_path = storage_dir / _result_filename(tool_use_id) + with file_path.open("w", encoding="utf-8") as f: f.write(content) + ensure_private_file(file_path) preview = content[: self._preview_chars] preview += f"\n\n... [truncated — full output ({len(content)} chars) saved to {file_path}]" - return ProcessedResult(content=preview, is_externalized=True, file_path=file_path) + return ProcessedResult(content=preview, is_externalized=True, file_path=str(file_path)) diff --git a/src/iac_code/ui/core/input_history.py b/src/iac_code/ui/core/input_history.py index 6618625..cd19097 100644 --- a/src/iac_code/ui/core/input_history.py +++ b/src/iac_code/ui/core/input_history.py @@ -2,13 +2,22 @@ from __future__ import annotations +import json import os +from pathlib import Path +from typing import Any + +from iac_code.utils.file_security import ensure_private_file + +_HISTORY_FORMAT = "iac-code-input-history-v1" class InputHistory: - """Stores and retrieves terminal input history from a plain text file. + """Stores and retrieves terminal input history from a JSONL file. - File format: one entry per line, most-recently-appended at the end. + New entries are stored as marked JSONL records, one entry per physical + line, most-recently-appended at the end. Legacy plain-line entries remain + readable. Attributes: _entries: In-memory list of history entries (oldest first). @@ -28,22 +37,39 @@ def __init__(self, history_file: str) -> None: # Persistence # ------------------------------------------------------------------ + @staticmethod + def _decode_line(line: str) -> str: + """Decode one history file line, accepting JSONL and legacy plain text.""" + try: + data: Any = json.loads(line) + except json.JSONDecodeError: + return line + if isinstance(data, dict) and data.get("format") == _HISTORY_FORMAT and isinstance(data.get("text"), str): + return data["text"] + return line + + @staticmethod + def _encode_entry(entry: str) -> str: + """Encode one history entry as a JSONL object.""" + return json.dumps({"format": _HISTORY_FORMAT, "text": entry}, ensure_ascii=False) + def _load(self) -> None: """Load entries from the history file if it exists.""" if not os.path.exists(self._file): return with open(self._file, encoding="utf-8") as f: for line in f: - entry = line.rstrip("\n") - if entry: - self._entries.append(entry) + raw = line.rstrip("\n") + if raw: + self._entries.append(self._decode_line(raw)) def _save(self) -> None: """Persist only non-session-only entries to the history file.""" with open(self._file, "w", encoding="utf-8") as f: for i, entry in enumerate(self._entries): if i not in self._session_only: - f.write(entry + "\n") + f.write(self._encode_entry(entry) + "\n") + ensure_private_file(Path(self._file)) # ------------------------------------------------------------------ # Mutation @@ -94,6 +120,10 @@ def search(self, prefix: str) -> list[str]: """Return entries whose text starts with *prefix*, most recent first.""" return [e for e in reversed(self._entries) if e.startswith(prefix)] + def entries(self) -> list[str]: + """Return all in-memory history entries, oldest first.""" + return list(self._entries) + # ------------------------------------------------------------------ # Navigation # ------------------------------------------------------------------ diff --git a/src/iac_code/ui/core/prompt_input.py b/src/iac_code/ui/core/prompt_input.py index 086f390..add10b1 100644 --- a/src/iac_code/ui/core/prompt_input.py +++ b/src/iac_code/ui/core/prompt_input.py @@ -114,6 +114,12 @@ def _get_text(self) -> str: """Return current buffer contents as a string.""" return "".join(self._buffer) + def insert_text(self, text: str) -> None: + """Insert text at the current cursor position.""" + if not text: + return + self._insert(text) + def attach_image(self, pc: "PastedContent") -> None: """Insert ``[Image #N]`` at the cursor and track the paste. @@ -209,7 +215,12 @@ def _handle_key(self, key_event: KeyEvent) -> None: self._cancelled = True return - # 4. Enter — accept suggestion and submit immediately + # 4. Shift+Enter → insert newline when the terminal reports Shift. + if key == "enter" and key_event.shift: + self._insert("\n") + return + + # 5. Enter — accept suggestion and submit immediately if key == "enter": if self._aggregator and self._aggregator.suggestions: result = self._aggregator.accept_selected() @@ -219,7 +230,7 @@ def _handle_key(self, key_event: KeyEvent) -> None: self._submitted = True return - # 5. Tab → accept ghost text + # 6. Tab → accept ghost text if key == "tab": if self._aggregator: result = self._aggregator.accept_ghost_text() @@ -229,11 +240,11 @@ def _handle_key(self, key_event: KeyEvent) -> None: return return - # 6. KeybindingManager resolution (Ctrl+R, Ctrl+P, etc.) + # 7. KeybindingManager resolution (Ctrl+R, Ctrl+P, etc.) if self._km.resolve(key_event): return - # 7. Up/Down with active suggestions → move selection + # 8. Up/Down with active suggestions → move selection # But if the user is actively navigating history, prioritize history. _in_history_nav = self._history and self._history.is_navigating if self._aggregator and self._aggregator.suggestions and not _in_history_nav: @@ -244,7 +255,7 @@ def _handle_key(self, key_event: KeyEvent) -> None: self._aggregator.move_selection(1) return - # 8. Up/Down with history + # 9. Up/Down with history if self._history: if key == "up": entry = self._history.navigate(-1, self._get_text()) @@ -261,7 +272,7 @@ def _handle_key(self, key_event: KeyEvent) -> None: self._set_text(entry) return - # 9. Line editing + # 10. Line editing if (ctrl and key == "a") or key == "home": self._cursor = 0 return @@ -307,7 +318,7 @@ def _handle_key(self, key_event: KeyEvent) -> None: self._text_changed = True return - # 10. Printable character insertion + # 11. Printable character insertion char = key_event.char if char and char.isprintable(): # Clear clipboard indicator on visible character input diff --git a/src/iac_code/ui/core/raw_input.py b/src/iac_code/ui/core/raw_input.py index b08cf03..ed7c77b 100644 --- a/src/iac_code/ui/core/raw_input.py +++ b/src/iac_code/ui/core/raw_input.py @@ -30,6 +30,11 @@ def query_cursor_row(fd: int, timeout: float = 0.1) -> int | None: # *after* the ESC byte, since we strip the ESC before parsing escape # sequences. _MOUSE_SGR_RE = re.compile(r"\[<(\d+);(\d+);(\d+)([Mm])") + _CSI_U_RE = re.compile(r"\[(\d+);(\d+)u") + _XTERM_MODIFY_OTHER_KEYS_RE = re.compile(r"\[27;(\d+);(\d+)~") + _MODIFIED_SPECIAL_KEY_RE = re.compile(r"\[(\d+);(\d+)~") + + _ENTER_CODEPOINTS = {10, 13} def query_cursor_row(fd: int, timeout: float = 0.1) -> int | None: """Send Device Status Report 6 and parse the cursor's 1-indexed row. @@ -110,6 +115,9 @@ def __enter__(self) -> "RawInputCapture": os.write(self._fd, b"\033[?2004h") # Enable focus reporting so we can detect terminal focus changes os.write(self._fd, b"\033[?1004h") + # Ask supporting terminals to report Shift+Enter distinctly. + os.write(self._fd, b"\033[>1u") + os.write(self._fd, b"\033[>4;2m") except OSError: # File descriptor may be invalid after interruption (e.g. double Ctrl+C) self._old_settings = None @@ -118,6 +126,9 @@ def __enter__(self) -> "RawInputCapture": def __exit__(self, exc_type, exc_val, exc_tb) -> None: try: + # Restore terminal modified-key reporting before leaving raw mode. + os.write(self._fd, b"\033[>4;0m") + os.write(self._fd, b"\033[ KeyEvent: if seq in _ESCAPE_SEQUENCES: return KeyEvent(key=_ESCAPE_SEQUENCES[seq], char="") + modified_key = RawInputCapture._parse_modified_key_sequence(seq) + if modified_key is not None: + return modified_key + # SGR mouse event — only wheel up/down are useful here. The # ``rest`` buffer may contain multiple back-to-back wheel events # when the user spins the wheel quickly; ``re.match`` picks up @@ -323,3 +338,46 @@ def _parse_escape_sequence(seq: str) -> KeyEvent: return KeyEvent(key=seq, char=seq, alt=True) return KeyEvent(key="unknown", char="") + + @staticmethod + def _event_from_codepoint(codepoint: int, modifier: int) -> KeyEvent | None: + """Build a KeyEvent from CSI-u / modifyOtherKeys codepoint data.""" + flags = max(modifier - 1, 0) + shift = bool(flags & 1) + alt = bool(flags & 2) + ctrl = bool(flags & 4) + + if codepoint in _ENTER_CODEPOINTS: + if shift and not alt and not ctrl: + return KeyEvent(key="enter", char="", shift=True) + return None + + if 32 <= codepoint <= 0x10FFFF: + key = chr(codepoint) + if ctrl: + key = key.lower() + return KeyEvent(key=key, char="", ctrl=ctrl, alt=alt, shift=shift) + + return None + + @staticmethod + def _parse_modified_key_sequence(seq: str) -> KeyEvent | None: + """Parse common terminal encodings for modified keys.""" + m = _CSI_U_RE.fullmatch(seq) + if m is not None: + codepoint = int(m.group(1)) + modifier = int(m.group(2)) + return RawInputCapture._event_from_codepoint(codepoint, modifier) + + m = _XTERM_MODIFY_OTHER_KEYS_RE.fullmatch(seq) + if m is not None: + modifier = int(m.group(1)) + codepoint = int(m.group(2)) + return RawInputCapture._event_from_codepoint(codepoint, modifier) + + m = _MODIFIED_SPECIAL_KEY_RE.fullmatch(seq) + if m is not None: + key_code = int(m.group(1)) + modifier = int(m.group(2)) + return RawInputCapture._event_from_codepoint(key_code, modifier) + return None diff --git a/src/iac_code/ui/core/raw_input_win.py b/src/iac_code/ui/core/raw_input_win.py index 3c16c0d..98e3503 100644 --- a/src/iac_code/ui/core/raw_input_win.py +++ b/src/iac_code/ui/core/raw_input_win.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re import time from iac_code.ui.core.key_event import KeyEvent @@ -31,6 +32,10 @@ "5~": "pageup", "6~": "pagedown", } +_CSI_U_RE = re.compile(r"(\d+);(\d+)u") +_XTERM_MODIFY_OTHER_KEYS_RE = re.compile(r"27;(\d+);(\d+)~") +_MODIFIED_SPECIAL_KEY_RE = re.compile(r"(\d+);(\d+)~") +_ENTER_CODEPOINTS = {10, 13} def _get_msvcrt(): @@ -161,5 +166,51 @@ def _read_csi_sequence(msvcrt) -> KeyEvent: buf += ch if ch.isalpha() or ch == "~": break + modified_key = RawInputCapture._parse_modified_key_sequence(buf) + if modified_key is not None: + return modified_key key_name = _ANSI_CSI_MAP.get(buf, "unknown") return KeyEvent(key=key_name, char="") + + @staticmethod + def _event_from_codepoint(codepoint: int, modifier: int) -> KeyEvent | None: + """Build a KeyEvent from CSI-u / modifyOtherKeys codepoint data.""" + flags = max(modifier - 1, 0) + shift = bool(flags & 1) + alt = bool(flags & 2) + ctrl = bool(flags & 4) + + if codepoint in _ENTER_CODEPOINTS: + if shift and not alt and not ctrl: + return KeyEvent(key="enter", char="", shift=True) + return None + + if 32 <= codepoint <= 0x10FFFF: + key = chr(codepoint) + if ctrl: + key = key.lower() + return KeyEvent(key=key, char="", ctrl=ctrl, alt=alt, shift=shift) + + return None + + @staticmethod + def _parse_modified_key_sequence(seq: str) -> KeyEvent | None: + """Parse common terminal encodings for modified keys.""" + m = _CSI_U_RE.fullmatch(seq) + if m is not None: + codepoint = int(m.group(1)) + modifier = int(m.group(2)) + return RawInputCapture._event_from_codepoint(codepoint, modifier) + + m = _XTERM_MODIFY_OTHER_KEYS_RE.fullmatch(seq) + if m is not None: + modifier = int(m.group(1)) + codepoint = int(m.group(2)) + return RawInputCapture._event_from_codepoint(codepoint, modifier) + + m = _MODIFIED_SPECIAL_KEY_RE.fullmatch(seq) + if m is not None: + key_code = int(m.group(1)) + modifier = int(m.group(2)) + return RawInputCapture._event_from_codepoint(key_code, modifier) + return None diff --git a/src/iac_code/ui/repl.py b/src/iac_code/ui/repl.py index 4b61e0a..e1e4f79 100644 --- a/src/iac_code/ui/repl.py +++ b/src/iac_code/ui/repl.py @@ -229,6 +229,7 @@ def __init__( cwd=self._original_cwd, permission_context=permission_context, permission_context_getter=lambda: self.store.get_state().permission_context, + auto_trigger_skills=skill_commands, ) self.renderer = Renderer( self.console, @@ -373,6 +374,10 @@ def _on_sigint() -> None: if not user_input: continue + if user_input.startswith("!"): + await self._handle_interactive_shell_escape(user_input) + continue + if self.command_registry.is_command(user_input): self._record_command_history(user_input) await self._handle_command(user_input) @@ -430,7 +435,10 @@ def _on_sigint() -> None: async def run_once(self, prompt: str) -> None: """Process a single prompt and exit (non-interactive mode).""" - if self.command_registry.is_command(prompt): + stripped_prompt = prompt.strip() + if stripped_prompt.startswith("!"): + await self._handle_shell_escape(stripped_prompt) + elif self.command_registry.is_command(prompt): await self._handle_command(prompt) else: await self._handle_chat(prompt) @@ -614,7 +622,7 @@ def _on_bracketed_paste(self, text: str) -> bool: def _open_history_search(self) -> bool: from iac_code.ui.dialogs.history_search import HistorySearch - messages = self.store.get_state().messages + messages = self._history_search_messages() dialog = HistorySearch( messages=messages, on_select=self._insert_text, @@ -624,6 +632,42 @@ def _open_history_search(self) -> bool: dialog.run() return True + def _history_search_messages(self) -> list[dict[str, str]]: + """Build searchable user-history rows from prompt history and conversation context.""" + entries: list[str] = [] + seen: set[str] = set() + + def add_text(text: str) -> None: + cleaned = text.strip() + if not cleaned or cleaned in seen: + return + seen.add(cleaned) + entries.append(cleaned) + + history = getattr(self, "_history", None) + if history is not None and hasattr(history, "entries"): + try: + for entry in history.entries(): + add_text(str(entry)) + except Exception: + pass + + try: + context_messages = self._agent_loop.context_manager.get_messages() + except Exception: + context_messages = [] + for msg in context_messages: + if getattr(msg, "role", None) != "user": + continue + get_text = getattr(msg, "get_text", None) + if callable(get_text): + add_text(get_text()) + continue + if isinstance(msg, dict) and msg.get("role") == "user": + add_text(str(msg.get("content", ""))) + + return [{"role": "user", "content": entry} for entry in entries] + def _open_quick_open(self) -> bool: from iac_code.ui.dialogs.quick_open import QuickOpen @@ -649,8 +693,14 @@ def _open_global_search(self) -> bool: return True def _insert_text(self, text: str) -> None: - """Insert text into the prompt input buffer (future enhancement).""" - pass # Will be enhanced when PromptInput supports external text insertion + """Insert text into the active prompt input buffer.""" + self._prompt_input.insert_text(text) + + async def _handle_interactive_shell_escape(self, user_input: str) -> None: + """Handle an interactive shell escape without adding it to prompt history.""" + await self._handle_shell_escape(user_input) + self._history.reset_navigation() + self._clear_cancel_state() def _expand_last_turn(self) -> bool: """Keybinding handler: open the verbose transcript view.""" @@ -661,6 +711,66 @@ def _expand_last_turn(self) -> bool: # Command handling # ------------------------------------------------------------------ + async def _handle_shell_escape(self, user_input: str) -> None: + """Execute a local shell command from a leading ! REPL input.""" + command = user_input[1:].strip() + if not command: + self.renderer.print_system_message(_("Usage: !"), style="yellow") + return + + tool = self.tool_registry.get("bash") + if tool is None: + self.renderer.print_system_message(_("Shell command support is unavailable."), style="red") + return + + tool_input = {"command": command} + if not await self._request_shell_escape_permission(tool, tool_input): + return + + from iac_code.tools.base import ToolContext + from iac_code.tools.tool_executor import ToolCallRequest, ToolExecutor + + executor = ToolExecutor(self.tool_registry) + results = await executor.execute_batch( + [ToolCallRequest(id="shell-escape", name="bash", input=tool_input)], + ToolContext(cwd=self._original_cwd), + ) + result = results[0] + self.renderer.print_system_message(f"$ {command}", style="dim") + output = result.content.rstrip() + if output: + self.renderer.print_system_message(output, style="red" if result.is_error else "white") + + async def _request_shell_escape_permission(self, tool, tool_input: dict) -> bool: + """Check permission for a display-only shell escape before execution.""" + permission_context = self.store.get_state().permission_context + if permission_context is not None: + from iac_code.services.permissions.pipeline import check_tool_permission + + permission = await check_tool_permission(tool, tool_input, permission_context) + else: + permission = await tool.check_permissions(tool_input, {"cwd": self._original_cwd}) + + if permission.behavior == "allow": + return True + if permission.behavior == "deny": + self.renderer.print_system_message(permission.message or _("Permission denied."), style="red") + return False + + from iac_code.types.stream_events import PermissionRequestEvent + + allowed = await self.renderer.prompt_permission( + PermissionRequestEvent( + tool_name="bash", + tool_input=tool_input, + tool_use_id="shell-escape", + permission_result=permission, + ) + ) + if not allowed: + self.renderer.print_system_message(_("Permission denied."), style="red") + return allowed + async def _handle_command(self, user_input: str) -> None: """Dispatch a slash command and print the result.""" is_skill_trigger = user_input.startswith("$") diff --git a/src/iac_code/utils/file_security.py b/src/iac_code/utils/file_security.py index a388e88..59824cd 100644 --- a/src/iac_code/utils/file_security.py +++ b/src/iac_code/utils/file_security.py @@ -34,6 +34,20 @@ def restrict_file_permissions(path: Path, *, directory: bool) -> None: return +def ensure_private_dir(path: Path) -> Path: + """Create a directory and restrict it to owner-only access.""" + path.mkdir(parents=True, exist_ok=True) + restrict_file_permissions(path, directory=True) + return path + + +def ensure_private_file(path: Path) -> Path: + """Restrict an existing file to owner-only access.""" + if path.exists(): + restrict_file_permissions(path, directory=False) + return path + + def _restrict_windows(path: Path, *, directory: bool) -> None: username = os.environ.get("USERNAME", "") if not username: diff --git a/src/iac_code/utils/image/store.py b/src/iac_code/utils/image/store.py index a0c7fa2..75e0637 100644 --- a/src/iac_code/utils/image/store.py +++ b/src/iac_code/utils/image/store.py @@ -8,6 +8,7 @@ from pathlib import Path from iac_code.config import get_config_dir +from iac_code.utils.file_security import ensure_private_dir from iac_code.utils.image.pasted_content import PastedContent IMAGE_STORE_DIR_NAME = "image-cache" @@ -40,8 +41,8 @@ def _session_dir(self) -> Path: def store(self, pc: PastedContent) -> str | None: if not pc.is_valid_image(): return None - d = self._session_dir() - d.mkdir(parents=True, exist_ok=True) + ensure_private_dir(_get_base_dir()) + d = ensure_private_dir(self._session_dir()) ext = (pc.media_type or "image/png").split("/")[-1] path = d / f"{pc.id}.{ext}" try: @@ -79,6 +80,7 @@ def cleanup_old_image_caches( base = _get_base_dir() if not base.exists(): return + ensure_private_dir(base) now = time.time() for entry in base.iterdir(): if not entry.is_dir() or entry.name == current_session_id: diff --git a/src/iac_code/utils/log.py b/src/iac_code/utils/log.py index f017f33..7649379 100644 --- a/src/iac_code/utils/log.py +++ b/src/iac_code/utils/log.py @@ -9,6 +9,7 @@ from loguru import logger from iac_code.config import get_config_dir +from iac_code.utils.file_security import ensure_private_dir, ensure_private_file _LOG_FORMAT = "{time:YYYY-MM-DDTHH:mm:ss.SSS} [{level:<5}] {name}:{function}:{line} - {message}" @@ -31,6 +32,7 @@ def _link_latest(log_dir: Path, log_file: Path) -> None: except OSError: try: shutil.copy2(log_file, latest) + ensure_private_file(latest) except OSError: pass @@ -66,8 +68,7 @@ def setup_logging( logger.remove() _runtime_debug_handler_ids = [] - log_dir = get_config_dir() / "logs" - log_dir.mkdir(parents=True, exist_ok=True) + log_dir = ensure_private_dir(get_config_dir() / "logs") log_file = log_dir / f"{session_id}.log" level = "DEBUG" if debug else "INFO" @@ -79,6 +80,7 @@ def setup_logging( ) _debug_enabled = debug _current_log_file = log_file + ensure_private_file(log_file) _link_latest(log_dir, log_file) @@ -103,8 +105,7 @@ def enable_debug_at_runtime(session_id: str) -> Path: """ global _debug_enabled, _current_log_file - log_dir = get_config_dir() / "logs" - log_dir.mkdir(parents=True, exist_ok=True) + log_dir = ensure_private_dir(get_config_dir() / "logs") log_file = log_dir / f"{session_id}.log" _current_log_file = log_file @@ -119,6 +120,7 @@ def enable_debug_at_runtime(session_id: str) -> Path: ) _runtime_debug_handler_ids.append(handler_id) _debug_enabled = True + ensure_private_file(log_file) _link_latest(log_dir, log_file) diff --git a/tests/acp/test_slash_registry.py b/tests/acp/test_slash_registry.py index f7358da..a932e76 100644 --- a/tests/acp/test_slash_registry.py +++ b/tests/acp/test_slash_registry.py @@ -144,17 +144,15 @@ async def _compact(): @pytest.mark.asyncio async def test_clear_success(registry: ACPSlashRegistry) -> None: agent_loop = MagicMock() - agent_loop.context_manager = MagicMock() result = await registry.execute("/clear", agent_loop=agent_loop) - agent_loop.context_manager.reset.assert_called_once() + agent_loop.reset.assert_called_once() assert "clear" in result.lower() or "清" in result @pytest.mark.asyncio async def test_clear_exception(registry: ACPSlashRegistry) -> None: agent_loop = MagicMock() - agent_loop.context_manager = MagicMock() - agent_loop.context_manager.reset.side_effect = RuntimeError("db error") + agent_loop.reset.side_effect = RuntimeError("db error") result = await registry.execute("/clear", agent_loop=agent_loop) assert "db error" in result diff --git a/tests/agent/test_agent_loop_new.py b/tests/agent/test_agent_loop_new.py index 8b369d7..b668c72 100644 --- a/tests/agent/test_agent_loop_new.py +++ b/tests/agent/test_agent_loop_new.py @@ -1,5 +1,5 @@ from types import SimpleNamespace -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -199,6 +199,297 @@ async def fake_stream(messages, system, tools=None, max_tokens=8192): assert len(assistant_blocks) == 1 assert assistant_blocks[0].text == "final" + async def test_max_turns_zero_emits_max_turns_without_provider_call(self, mock_provider, mock_registry): + mock_provider.stream = AsyncMock() + + loop = AgentLoop( + provider_manager=mock_provider, + system_prompt="test", + tool_registry=mock_registry, + max_turns=0, + ) + loop.context_manager = MagicMock() + loop.context_manager.needs_compaction.return_value = False + + events = [e async for e in loop.run_streaming("Hi")] + + assert any(isinstance(e, MessageEndEvent) and e.stop_reason == "max_turns" for e in events) + mock_provider.stream.assert_not_called() + + async def test_tool_use_exhaustion_emits_max_turns_after_tool_result(self, mock_provider, mock_registry): + async def fake_stream(messages, system, tools=None, max_tokens=8192): + yield MessageStartEvent(message_id="m1") + yield ToolUseStartEvent(tool_use_id="toolu_1", name="read_file") + yield ToolUseEndEvent(tool_use_id="toolu_1", name="read_file", input={"path": "a.txt"}) + yield MessageEndEvent(stop_reason="tool_use", usage=Usage()) + + mock_provider.stream = fake_stream + mock_registry.list_tools.return_value = [SimpleNamespace(name="read_file", description="Read", input_schema={})] + + loop = AgentLoop( + provider_manager=mock_provider, + system_prompt="test", + tool_registry=mock_registry, + max_turns=1, + ) + loop._result_storage = MagicMock() + loop._result_storage.process.return_value = SimpleNamespace(content="processed result") + loop.context_manager = MagicMock() + loop.context_manager.get_api_messages.return_value = [] + loop.context_manager.needs_compaction.return_value = False + loop._tool_executor.execute_batch = AsyncMock(return_value=[ToolResult(content="raw result", is_error=False)]) + + events = [e async for e in loop.run_streaming("Hi")] + + event_types = [e.type for e in events] + assert "tool_result" in event_types + assert isinstance(events[-1], MessageEndEvent) + assert events[-1].stop_reason == "max_turns" + + async def test_normal_completion_does_not_emit_synthetic_max_turns(self, mock_provider, mock_registry): + async def fake_stream(messages, system, tools=None, max_tokens=8192): + yield MessageStartEvent(message_id="m1") + yield TextDeltaEvent(text="Hello!") + yield MessageEndEvent(stop_reason="end_turn", usage=Usage()) + + mock_provider.stream = fake_stream + + loop = AgentLoop( + provider_manager=mock_provider, + system_prompt="test", + tool_registry=mock_registry, + max_turns=1, + ) + + events = [e async for e in loop.run_streaming("Hi")] + + assert not any(isinstance(e, MessageEndEvent) and e.stop_reason == "max_turns" for e in events) + + async def test_auto_trigger_injects_skill_before_provider_call(self, mock_provider, mock_registry): + from iac_code.commands.registry import PromptCommand + from iac_code.skills.frontmatter import SkillFrontmatter + from iac_code.skills.skill_definition import SkillDefinition + from iac_code.types.skill_source import SkillSource + + fm = SkillFrontmatter(description="demo", auto_trigger={"script": "auto_trigger.py"}) + skill = SkillDefinition( + name="demo", + description="demo", + frontmatter=fm, + content="Demo skill prompt", + source=SkillSource.BUNDLED, + skill_root="/tmp", + ) + command = PromptCommand(name="demo", description="demo", skill=skill, source=SkillSource.BUNDLED) + + async def fake_process(prompt, skills, *, loaded_skill_names, context_messages=None, session_id=""): + loaded_skill_names.add("demo") + return [ + SimpleNamespace( + skill_name="demo", + new_messages=[{"role": "user", "content": "demo\n\nDemo skill prompt"}], + context_modifier=None, + ) + ] + + async def fake_stream(messages, system, tools=None, max_tokens=8192): + assert messages[0].content.startswith("demo") + assert messages[1].content == "please match" + yield MessageStartEvent(message_id="m1") + yield TextDeltaEvent(text="ok") + yield MessageEndEvent(stop_reason="end_turn", usage=Usage()) + + mock_provider.stream = fake_stream + loop = AgentLoop( + provider_manager=mock_provider, + system_prompt="test", + tool_registry=mock_registry, + auto_trigger_skills=[command], + ) + + with patch("iac_code.skills.auto_trigger.process_auto_triggered_skills", fake_process): + events = [e async for e in loop.run_streaming("please match")] + + assert any(isinstance(e, TextDeltaEvent) for e in events) + assert loop._auto_loaded_skills == {"demo"} + + async def test_auto_trigger_persists_injected_message_before_user_message(self, mock_provider, mock_registry): + from iac_code.commands.registry import PromptCommand + from iac_code.skills.frontmatter import SkillFrontmatter + from iac_code.skills.skill_definition import SkillDefinition + from iac_code.types.skill_source import SkillSource + + fm = SkillFrontmatter(description="demo", auto_trigger={"script": "auto_trigger.py"}) + skill = SkillDefinition( + name="demo", + description="demo", + frontmatter=fm, + content="Demo skill prompt", + source=SkillSource.BUNDLED, + skill_root="/tmp", + ) + command = PromptCommand(name="demo", description="demo", skill=skill, source=SkillSource.BUNDLED) + session_storage = MagicMock() + + async def fake_process(prompt, skills, *, loaded_skill_names, context_messages=None, session_id=""): + loaded_skill_names.add("demo") + return [ + SimpleNamespace( + skill_name="demo", + new_messages=[{"role": "user", "content": "demo\n\nDemo skill prompt"}], + context_modifier=None, + ) + ] + + async def fake_stream(messages, system, tools=None, max_tokens=8192): + assert messages[0].content.startswith("demo") + assert messages[1].content == "please match" + yield MessageStartEvent(message_id="m1") + yield MessageEndEvent(stop_reason="end_turn", usage=Usage()) + + mock_provider.stream = fake_stream + loop = AgentLoop( + provider_manager=mock_provider, + system_prompt="test", + tool_registry=mock_registry, + session_storage=session_storage, + session_id="session-a", + auto_trigger_skills=[command], + ) + + with patch("iac_code.skills.auto_trigger.process_auto_triggered_skills", fake_process): + await loop.run("please match") + + persisted_messages = [call.args[2] for call in session_storage.append.call_args_list] + assert persisted_messages[0].content.startswith("demo") + assert persisted_messages[1].content == "please match" + + async def test_auto_trigger_resume_keeps_persisted_skill_idempotent(self, tmp_path, mock_provider, mock_registry): + from iac_code.commands.registry import PromptCommand + from iac_code.services.session_storage import SessionStorage + from iac_code.skills.frontmatter import SkillFrontmatter + from iac_code.skills.skill_definition import SkillDefinition + from iac_code.types.skill_source import SkillSource + + skill_root = tmp_path / "skill" + skill_root.mkdir() + (skill_root / "auto_trigger.py").write_text( + "ENABLE_AUTO_TRIGGER = True\ndef should_trigger(prompt):\n return 'match me' in prompt\n", + encoding="utf-8", + ) + fm = SkillFrontmatter(description="demo", auto_trigger={"script": "auto_trigger.py"}) + skill = SkillDefinition( + name="demo", + description="demo", + frontmatter=fm, + content="Demo skill prompt", + source=SkillSource.BUNDLED, + skill_root=str(skill_root), + ) + command = PromptCommand(name="demo", description="demo", skill=skill, source=SkillSource.BUNDLED) + storage = SessionStorage(projects_dir=tmp_path / "projects") + cwd = str(tmp_path / "cwd") + session_id = "session-a" + + async def first_stream(messages, system, tools=None, max_tokens=8192): + assert messages[0].content.startswith("demo") + assert messages[1].content == "please match me" + yield MessageStartEvent(message_id="m1") + yield MessageEndEvent(stop_reason="end_turn", usage=Usage()) + + mock_provider.stream = first_stream + first_loop = AgentLoop( + provider_manager=mock_provider, + system_prompt="test", + tool_registry=mock_registry, + session_storage=storage, + session_id=session_id, + cwd=cwd, + auto_trigger_skills=[command], + ) + + await first_loop.run("please match me") + loaded = storage.load(cwd, session_id) + assert sum("demo" in message.get_text() for message in loaded) == 1 + + async def resumed_stream(messages, system, tools=None, max_tokens=8192): + assert sum("demo" in message.content for message in messages) == 1 + assert messages[-1].content == "please match me again" + yield MessageStartEvent(message_id="m2") + yield MessageEndEvent(stop_reason="end_turn", usage=Usage()) + + mock_provider.stream = resumed_stream + resumed_loop = AgentLoop( + provider_manager=mock_provider, + system_prompt="test", + tool_registry=mock_registry, + session_storage=storage, + session_id=session_id, + resume_messages=loaded, + cwd=cwd, + auto_trigger_skills=[command], + ) + + await resumed_loop.run("please match me again") + reloaded = storage.load(cwd, session_id) + + assert sum("demo" in message.get_text() for message in reloaded) == 1 + assert resumed_loop._auto_loaded_skills == {"demo"} + + async def test_auto_trigger_does_not_repeat_loaded_skill(self, mock_provider, mock_registry): + from iac_code.commands.registry import PromptCommand + from iac_code.skills.frontmatter import SkillFrontmatter + from iac_code.skills.skill_definition import SkillDefinition + from iac_code.types.skill_source import SkillSource + + fm = SkillFrontmatter(description="demo", auto_trigger={"script": "auto_trigger.py"}) + skill = SkillDefinition( + name="demo", + description="demo", + frontmatter=fm, + content="Demo skill prompt", + source=SkillSource.BUNDLED, + skill_root="/tmp", + ) + command = PromptCommand(name="demo", description="demo", skill=skill, source=SkillSource.BUNDLED) + calls = 0 + + async def fake_process(prompt, skills, *, loaded_skill_names, context_messages=None, session_id=""): + nonlocal calls + calls += 1 + loaded_skill_names.add("demo") + return [ + SimpleNamespace( + skill_name="demo", + new_messages=[{"role": "user", "content": "demo\n\nDemo skill prompt"}], + context_modifier=None, + ) + ] + + async def fake_stream(messages, system, tools=None, max_tokens=8192): + yield MessageStartEvent(message_id="m1") + yield MessageEndEvent(stop_reason="end_turn", usage=Usage()) + + mock_provider.stream = fake_stream + loop = AgentLoop( + provider_manager=mock_provider, + system_prompt="test", + tool_registry=mock_registry, + auto_trigger_skills=[command], + ) + + with patch("iac_code.skills.auto_trigger.process_auto_triggered_skills", fake_process): + await loop.run("first") + await loop.run("second") + + injected = [ + message + for message in loop.context_manager.get_messages() + if isinstance(message.content, str) and "demo" in message.content + ] + assert calls == 1 + assert len(injected) == 1 + @pytest.mark.asyncio class TestAgentLoopCompaction: @@ -273,13 +564,25 @@ def test_reset_and_get_context_usage_delegate(self, mock_provider, mock_registry loop = AgentLoop(provider_manager=mock_provider, system_prompt="test", tool_registry=mock_registry) loop.context_manager = MagicMock() loop.context_manager.get_usage.return_value = {"total_tokens": 10} + loop._auto_loaded_skills.add("iac-aliyun") loop.reset() usage = loop.get_context_usage() loop.context_manager.reset.assert_called_once() + assert loop._auto_loaded_skills == set() assert usage == {"total_tokens": 10} + def test_replace_session_clears_auto_loaded_skills(self, mock_provider, mock_registry): + from iac_code.agent.message import Message + + loop = AgentLoop(provider_manager=mock_provider, system_prompt="test", tool_registry=mock_registry) + loop._auto_loaded_skills.add("demo") + + loop.replace_session("session-b", [Message(role="user", content="new session")]) + + assert loop._auto_loaded_skills == set() + class TestAgentLoopSetProvider: def test_set_provider_preserves_messages(self, mock_provider, mock_registry): diff --git a/tests/cli/test_headless.py b/tests/cli/test_headless.py index da2bb66..286634d 100644 --- a/tests/cli/test_headless.py +++ b/tests/cli/test_headless.py @@ -19,7 +19,12 @@ ErrorEvent, MessageEndEvent, PermissionRequestEvent, + StackInstancesProgressEvent, + StackProgressEvent, + SubAgentToolEvent, TextDeltaEvent, + ToolResultEvent, + ToolUseStartEvent, Usage, ) @@ -276,6 +281,58 @@ def test_output_format_passed_to_headless(self): call_kwargs = mock_runner.call_args[1] assert call_kwargs["output_format"] == OutputFormat.JSON + def test_invalid_output_format_exits_before_headless_runner(self): + from iac_code.cli.main import app + + with patch("iac_code.cli.headless.HeadlessRunner") as mock_runner: + result = runner_cli.invoke(app, ["-p", "hello", "--output-format", "invalid"]) + + assert result.exit_code != 0 + assert "Invalid --output-format 'invalid'" in result.output + assert "Valid values: text, json, stream-json" in result.output + assert "Traceback" not in result.output + mock_runner.assert_not_called() + + def test_invalid_output_format_exits_before_provider_env_lookup(self, monkeypatch): + from iac_code.cli.main import app + + monkeypatch.setenv("IAC_CODE_PROVIDER", "NotAProvider") + with patch("iac_code.cli.headless.HeadlessRunner") as mock_runner: + result = runner_cli.invoke(app, ["-p", "hello", "--output-format", "invalid"]) + + assert result.exit_code != 0 + assert "Invalid --output-format 'invalid'" in result.output + assert "Invalid IAC_CODE_PROVIDER" not in result.output + mock_runner.assert_not_called() + + def test_output_format_is_case_insensitive(self): + from iac_code.cli.main import app + + with patch("iac_code.cli.headless.HeadlessRunner") as mock_runner: + mock_instance = MagicMock() + mock_instance.run = AsyncMock(return_value=0) + mock_runner.return_value = mock_instance + + result = runner_cli.invoke(app, ["-p", "hello", "--output-format", "TEXT"]) + + assert result.exit_code == 0 + call_kwargs = mock_runner.call_args[1] + assert call_kwargs["output_format"] == OutputFormat.TEXT + + def test_verbose_passed_to_headless(self): + from iac_code.cli.main import app + + with patch("iac_code.cli.headless.HeadlessRunner") as mock_runner: + mock_instance = MagicMock() + mock_instance.run = AsyncMock(return_value=0) + mock_runner.return_value = mock_instance + + result = runner_cli.invoke(app, ["-p", "hello", "--verbose"]) + + assert result.exit_code == 0 + call_kwargs = mock_runner.call_args[1] + assert call_kwargs["verbose"] is True + def test_max_turns_passed_to_headless(self): from iac_code.cli.main import app @@ -309,6 +366,16 @@ def test_invalid_provider_env_prints_error_not_traceback(self, monkeypatch): assert result.exit_code != 0 assert "Invalid IAC_CODE_PROVIDER" in result.output + def test_invalid_permission_mode_exits_before_headless_runner(self): + from iac_code.cli.main import app + + with patch("iac_code.cli.headless.HeadlessRunner") as mock_runner: + result = runner_cli.invoke(app, ["-p", "write", "--permission-mode", "nonsense"]) + + assert result.exit_code != 0 + assert "Invalid --permission-mode 'nonsense'" in result.output + mock_runner.assert_not_called() + @pytest.mark.asyncio async def test_permission_without_response_future_is_ignored_and_writer_finalized(): @@ -343,6 +410,222 @@ async def test_permission_without_response_future_is_ignored_and_writer_finalize writer.finalize.assert_called_once() +@pytest.mark.asyncio +async def test_verbose_progress_writes_tool_events_to_progress_stream(): + stdout = io.StringIO() + progress = io.StringIO() + runner = HeadlessRunner( + model="test-model", + output_format=OutputFormat.TEXT, + output_stream=stdout, + verbose=True, + progress_stream=progress, + ) + + events = [ + ToolUseStartEvent(tool_use_id="tu_1", name="bash"), + TextDeltaEvent(text="done"), + ToolResultEvent(tool_use_id="tu_1", tool_name="bash", result="ok", is_error=False), + MessageEndEvent(stop_reason="end_turn", usage=Usage()), + ] + + with patch.object(runner, "_create_agent_loop") as mock_create: + mock_loop = AsyncMock() + mock_loop.run_streaming = lambda prompt: _fake_stream(*events) + mock_create.return_value = mock_loop + + exit_code = await runner.run("test prompt") + + assert exit_code == EXIT_OK + assert stdout.getvalue() == "done\n" + progress_output = progress.getvalue() + assert "Tool started: bash" in progress_output + assert "Tool finished: bash" in progress_output + + +@pytest.mark.asyncio +async def test_verbose_progress_preserves_json_stdout(): + stdout = io.StringIO() + progress = io.StringIO() + runner = HeadlessRunner( + model="test-model", + output_format=OutputFormat.JSON, + output_stream=stdout, + verbose=True, + progress_stream=progress, + ) + + events = [ + ToolUseStartEvent(tool_use_id="tu_1", name="bash"), + TextDeltaEvent(text="done"), + ToolResultEvent(tool_use_id="tu_1", tool_name="bash", result="ok", is_error=False), + MessageEndEvent(stop_reason="end_turn", usage=Usage(input_tokens=1, output_tokens=2)), + ] + + with patch.object(runner, "_create_agent_loop") as mock_create: + mock_loop = AsyncMock() + mock_loop.run_streaming = lambda prompt: _fake_stream(*events) + mock_create.return_value = mock_loop + + exit_code = await runner.run("test prompt") + + assert exit_code == EXIT_OK + parsed = json.loads(stdout.getvalue()) + assert parsed["text"] == "done" + assert parsed["tool_uses"][0]["name"] == "bash" + assert "Tool started: bash" in progress.getvalue() + assert "Tool started: bash" not in stdout.getvalue() + + +@pytest.mark.asyncio +async def test_verbose_progress_preserves_stream_json_stdout(): + stdout = io.StringIO() + progress = io.StringIO() + runner = HeadlessRunner( + model="test-model", + output_format=OutputFormat.STREAM_JSON, + output_stream=stdout, + verbose=True, + progress_stream=progress, + ) + + events = [ + ToolUseStartEvent(tool_use_id="tu_1", name="bash"), + TextDeltaEvent(text="done"), + ToolResultEvent(tool_use_id="tu_1", tool_name="bash", result="ok", is_error=False), + MessageEndEvent(stop_reason="end_turn", usage=Usage(input_tokens=1, output_tokens=2)), + ] + + with patch.object(runner, "_create_agent_loop") as mock_create: + mock_loop = AsyncMock() + mock_loop.run_streaming = lambda prompt: _fake_stream(*events) + mock_create.return_value = mock_loop + + exit_code = await runner.run("test prompt") + + assert exit_code == EXIT_OK + lines = stdout.getvalue().splitlines() + assert [json.loads(line)["type"] for line in lines] == [ + "tool_use_start", + "text_delta", + "tool_result", + "message_end", + ] + assert "Tool started: bash" in progress.getvalue() + assert "Tool started: bash" not in stdout.getvalue() + + +@pytest.mark.asyncio +async def test_verbose_progress_marks_failed_tool_results(): + stdout = io.StringIO() + progress = io.StringIO() + runner = HeadlessRunner( + model="test-model", + output_format=OutputFormat.TEXT, + output_stream=stdout, + verbose=True, + progress_stream=progress, + ) + + events = [ + ToolResultEvent(tool_use_id="tu_1", tool_name="bash", result="boom", is_error=True), + MessageEndEvent(stop_reason="end_turn", usage=Usage()), + ] + + with patch.object(runner, "_create_agent_loop") as mock_create: + mock_loop = AsyncMock() + mock_loop.run_streaming = lambda prompt: _fake_stream(*events) + mock_create.return_value = mock_loop + + exit_code = await runner.run("test prompt") + + assert exit_code == EXIT_OK + assert "Tool failed: bash" in progress.getvalue() + + +@pytest.mark.asyncio +async def test_verbose_false_does_not_write_progress_stream(): + stdout = io.StringIO() + progress = io.StringIO() + runner = HeadlessRunner( + model="test-model", + output_format=OutputFormat.TEXT, + output_stream=stdout, + verbose=False, + progress_stream=progress, + ) + + events = [ + ToolUseStartEvent(tool_use_id="tu_1", name="bash"), + ToolResultEvent(tool_use_id="tu_1", tool_name="bash", result="ok", is_error=False), + MessageEndEvent(stop_reason="end_turn", usage=Usage()), + ] + + with patch.object(runner, "_create_agent_loop") as mock_create: + mock_loop = AsyncMock() + mock_loop.run_streaming = lambda prompt: _fake_stream(*events) + mock_create.return_value = mock_loop + + exit_code = await runner.run("test prompt") + + assert exit_code == EXIT_OK + assert progress.getvalue() == "" + + +@pytest.mark.asyncio +async def test_verbose_progress_writes_subagent_and_stack_events(): + stdout = io.StringIO() + progress = io.StringIO() + runner = HeadlessRunner( + model="test-model", + output_format=OutputFormat.TEXT, + output_stream=stdout, + verbose=True, + progress_stream=progress, + ) + + events = [ + SubAgentToolEvent(parent_tool_use_id="parent", child_tool_name="read_file", child_tool_input={}), + SubAgentToolEvent( + parent_tool_use_id="parent", + child_tool_name="read_file", + child_tool_input={}, + is_done=True, + ), + StackProgressEvent( + stack_id="sid", + stack_name="stack", + status="CREATE_IN_PROGRESS", + progress_percentage=42.5, + resources=[], + elapsed_seconds=3, + ), + StackInstancesProgressEvent( + stack_group_name="group", + operation_id="op", + status="RUNNING", + progress_percentage=67, + instances=[], + elapsed_seconds=4, + ), + MessageEndEvent(stop_reason="end_turn", usage=Usage()), + ] + + with patch.object(runner, "_create_agent_loop") as mock_create: + mock_loop = AsyncMock() + mock_loop.run_streaming = lambda prompt: _fake_stream(*events) + mock_create.return_value = mock_loop + + exit_code = await runner.run("test prompt") + + assert exit_code == EXIT_OK + progress_output = progress.getvalue() + assert "Child tool started: read_file" in progress_output + assert "Child tool finished: read_file" in progress_output + assert "Stack stack: CREATE_IN_PROGRESS (42.5%)" in progress_output + assert "Stack group group: RUNNING (67%)" in progress_output + + class FakeToolRegistry: def __init__(self): self.registered = [] diff --git a/tests/cli/test_output_formats.py b/tests/cli/test_output_formats.py index bfd7c54..8486dce 100644 --- a/tests/cli/test_output_formats.py +++ b/tests/cli/test_output_formats.py @@ -112,6 +112,21 @@ def test_error_event_captured(self) -> None: result = json.loads(stream.getvalue()) assert result["error"] == "something went wrong" + def test_synthetic_max_turns_does_not_overwrite_previous_usage(self) -> None: + stream = io.StringIO() + writer = JsonWriter(stream) + writer.handle(MessageEndEvent(stop_reason="tool_use", usage=Usage(input_tokens=10, output_tokens=5))) + writer.handle(MessageEndEvent(stop_reason="max_turns", usage=Usage())) + writer.finalize() + + result = json.loads(stream.getvalue()) + assert result["usage"] == { + "input_tokens": 10, + "output_tokens": 5, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + } + # --------------------------------------------------------------------------- # TestStreamJsonWriter diff --git a/tests/commands/test_clear.py b/tests/commands/test_clear.py index c34395d..a2a7398 100644 --- a/tests/commands/test_clear.py +++ b/tests/commands/test_clear.py @@ -32,6 +32,18 @@ async def test_clear_uses_kwargs_store(): store.set_state.assert_called_with(messages=[]) +@pytest.mark.asyncio +async def test_clear_resets_agent_loop(): + agent_loop = MagicMock() + repl = MagicMock(_agent_loop=agent_loop) + context = MagicMock(repl=repl) + context.console = None + + await clear_command(context=context) + + agent_loop.reset.assert_called_once() + + @pytest.mark.asyncio async def test_clear_with_console_writes_ansi_and_banner(monkeypatch): store = MagicMock() diff --git a/tests/memory/test_memory_manager.py b/tests/memory/test_memory_manager.py index 0f894c4..03c5083 100644 --- a/tests/memory/test_memory_manager.py +++ b/tests/memory/test_memory_manager.py @@ -1,3 +1,5 @@ +import sys + import pytest from iac_code.memory.memory_manager import MemoryManager @@ -35,6 +37,57 @@ def test_index(self, manager): manager.save("idx", content="Data", memory_type="project", description="Index test") assert "idx" in manager.get_index_content() + @pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes are not meaningful on Windows") + def test_memory_files_are_owner_only(self, manager, tmp_path): + manager.save("private", content="Data", memory_type="project", description="Index test") + + memory_file = tmp_path / "private.md" + index_file = tmp_path / "MEMORY.md" + assert oct(tmp_path.stat().st_mode & 0o777) == "0o700" + assert oct(memory_file.stat().st_mode & 0o777) == "0o600" + assert oct(index_file.stat().st_mode & 0o777) == "0o600" + def test_prompt_content(self, manager): manager.save("m1", content="Rule 1", memory_type="feedback", description="R") assert "Rule 1" in manager.get_prompt_content() + + @pytest.mark.parametrize("name", ["", " ", ".", "..", "../escape", "a/b", r"a\b", "/tmp/escape", "bad name"]) + def test_save_rejects_invalid_memory_names(self, manager, tmp_path, name): + outside = tmp_path.parent / "escape.md" + + with pytest.raises(ValueError, match="Invalid memory name"): + manager.save(name, content="X", memory_type="user", description="bad") + + assert not outside.exists() + + @pytest.mark.parametrize("name", ["../escape", "a/b", r"a\b", "/tmp/escape"]) + def test_load_and_delete_reject_invalid_memory_names(self, manager, name): + with pytest.raises(ValueError, match="Invalid memory name"): + manager.load(name) + with pytest.raises(ValueError, match="Invalid memory name"): + manager.delete(name) + + @pytest.mark.parametrize("name", ["project-note", "user_1", "release.2026"]) + def test_accepts_safe_memory_names(self, manager, name): + manager.save(name, content="safe", memory_type="user", description="ok") + + mem = manager.load(name) + + assert mem is not None + assert mem["content"] == "safe" + + @pytest.mark.parametrize("name", ["MEMORY", "memory"]) + def test_rejects_reserved_index_memory_names(self, manager, name): + with pytest.raises(ValueError, match="Invalid memory name"): + manager.save(name, content="X", memory_type="user", description="reserved") + + def test_legacy_invalid_memory_file_does_not_break_listing_or_index_update(self, manager, tmp_path): + legacy = tmp_path / "old memory.md" + legacy.write_text("---\nname: old memory\ndescription: Legacy\ntype: user\n---\n\nlegacy content\n") + + memories = manager.list_memories() + manager.save("new-safe", content="new content", memory_type="user", description="New") + + assert [memory["name"] for memory in memories] == ["old memory"] + assert "old memory" in manager.get_index_content() + assert "new-safe" in manager.get_index_content() diff --git a/tests/memory/test_memory_tools.py b/tests/memory/test_memory_tools.py index f931a5b..f3fcc92 100644 --- a/tests/memory/test_memory_tools.py +++ b/tests/memory/test_memory_tools.py @@ -122,5 +122,24 @@ def save(self, *, name, content, memory_type, description): assert result.is_error is True assert result.content == "disk full" + async def test_write_memory_returns_error_for_invalid_name(self, tmp_path): + from iac_code.memory.memory_manager import MemoryManager + + tool = WriteMemoryTool(MemoryManager(memory_dir=str(tmp_path))) + + result = await tool.execute( + tool_input={ + "name": "../escape", + "content": "escaped", + "memory_type": "user", + "description": "bad", + }, + context=ToolContext(), + ) + + assert result.is_error is True + assert "Invalid memory name" in result.content + assert not (tmp_path.parent / "escape.md").exists() + async def test_is_read_only(self): assert WriteMemoryTool(FakeMemoryManager()).is_read_only() is False diff --git a/tests/services/permissions/test_loader.py b/tests/services/permissions/test_loader.py index b577ffa..c3e3ac4 100644 --- a/tests/services/permissions/test_loader.py +++ b/tests/services/permissions/test_loader.py @@ -2,6 +2,7 @@ from __future__ import annotations +import pytest import yaml from iac_code.services.permissions.loader import load_permission_context, load_settings_permissions @@ -65,6 +66,30 @@ def test_cli_overrides(self, tmp_path, monkeypatch): assert "bash(rm *)" in ctx.deny_rules.get("cli_arg", []) assert ctx.mode == PermissionMode.BYPASS_PERMISSIONS + def test_parse_cli_permission_mode_rejects_invalid_value(self): + from iac_code.services.permissions.loader import parse_cli_permission_mode + + with pytest.raises(ValueError, match="Invalid --permission-mode 'nonsense'"): + parse_cli_permission_mode("nonsense") + + def test_parse_cli_permission_mode_error_is_translatable(self, monkeypatch): + import iac_code.services.permissions.loader as loader + + monkeypatch.setattr(loader, "_", lambda msg: f"TRANSLATED:{msg}", raising=False) + + with pytest.raises(ValueError) as exc: + loader.parse_cli_permission_mode("nonsense") + + assert str(exc.value).startswith("TRANSLATED:Invalid --permission-mode") + + def test_load_permission_context_rejects_invalid_cli_mode(self, tmp_path, monkeypatch): + monkeypatch.setattr( + "iac_code.services.permissions.loader._get_global_settings_path", lambda: tmp_path / "nonexistent.yml" + ) + + with pytest.raises(ValueError, match="Invalid --permission-mode 'nonsense'"): + load_permission_context(str(tmp_path), cli_mode="nonsense") + def test_project_settings_merge(self, tmp_path, monkeypatch): monkeypatch.setattr( "iac_code.services.permissions.loader._get_global_settings_path", lambda: tmp_path / "nonexistent.yml" diff --git a/tests/services/test_session_storage.py b/tests/services/test_session_storage.py index d1c3f58..8dac0fc 100644 --- a/tests/services/test_session_storage.py +++ b/tests/services/test_session_storage.py @@ -1,4 +1,5 @@ import json +import sys import pytest @@ -46,6 +47,14 @@ def test_append_round_trip(self, storage): assert len(loaded) == 2 assert loaded[0].get_text() == "First" + @pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes are not meaningful on Windows") + def test_append_writes_owner_only_session_file(self, storage): + storage.append(CWD, "private-session", Message(role="user", content="hi"), git_branch=None) + path = storage.session_path(CWD, "private-session") + + assert oct(path.parent.stat().st_mode & 0o777) == "0o700" + assert oct(path.stat().st_mode & 0o777) == "0o600" + def test_load_nonexistent(self, storage): assert storage.load(CWD, "nope") == [] diff --git a/tests/skills/bundled/test_iac_skill.py b/tests/skills/bundled/test_iac_skill.py index 1a1f94b..6c55b37 100644 --- a/tests/skills/bundled/test_iac_skill.py +++ b/tests/skills/bundled/test_iac_skill.py @@ -19,3 +19,9 @@ def test_iac_skill_has_description(self): skills = get_bundled_skills() iac_skill = next(s for s in skills if s.name == "iac-aliyun") assert len(iac_skill.description) > 0 + + def test_iac_skill_has_auto_trigger_metadata(self): + init_bundled_skills() + skills = get_bundled_skills() + iac_skill = next(s for s in skills if s.name == "iac-aliyun") + assert iac_skill.auto_trigger == {"script": "auto_trigger.py"} diff --git a/tests/skills/test_auto_trigger.py b/tests/skills/test_auto_trigger.py new file mode 100644 index 0000000..129542b --- /dev/null +++ b/tests/skills/test_auto_trigger.py @@ -0,0 +1,219 @@ +from pathlib import Path + +import pytest + +from iac_code.agent.message import Message +from iac_code.commands.registry import PromptCommand +from iac_code.skills.auto_trigger import find_auto_triggered_skills, has_skill_tag +from iac_code.skills.frontmatter import SkillFrontmatter +from iac_code.skills.skill_definition import SkillDefinition +from iac_code.types.skill_source import SkillSource + + +def _command(tmp_path: Path, *, source: SkillSource = SkillSource.BUNDLED) -> PromptCommand: + script = tmp_path / "auto_trigger.py" + script.write_text( + "ENABLE_AUTO_TRIGGER = True\ndef should_trigger(prompt):\n return 'match me' in prompt\n", + encoding="utf-8", + ) + fm = SkillFrontmatter(description="demo", auto_trigger={"script": "auto_trigger.py"}) + skill = SkillDefinition( + name="demo", + description="demo", + frontmatter=fm, + content="Demo prompt", + source=source, + skill_root=str(tmp_path), + ) + return PromptCommand(name="demo", description="demo", skill=skill, source=source) + + +def _mismatched_command(tmp_path: Path) -> PromptCommand: + command = _command(tmp_path) + return PromptCommand( + name=command.name, + description=command.description, + skill=command.skill, + source=SkillSource.PROJECT, + ) + + +def _bundled_command_with_project_skill(tmp_path: Path) -> PromptCommand: + command = _command(tmp_path, source=SkillSource.PROJECT) + return PromptCommand( + name=command.name, + description=command.description, + skill=command.skill, + source=SkillSource.BUNDLED, + ) + + +def test_has_skill_tag_detects_loaded_skill(): + assert has_skill_tag("iac-aliyun", "iac-aliyun") + assert not has_skill_tag("other", "iac-aliyun") + + +def test_find_auto_triggered_skills_loads_bundled_script(tmp_path): + matches = find_auto_triggered_skills("please match me", [_command(tmp_path)], loaded_skill_names=set()) + assert [cmd.name for cmd in matches] == ["demo"] + + +def test_find_auto_triggered_skills_ignores_project_scripts(tmp_path): + matches = find_auto_triggered_skills( + "please match me", + [_command(tmp_path, source=SkillSource.PROJECT)], + loaded_skill_names=set(), + ) + assert matches == [] + + +def test_find_auto_triggered_skills_ignores_project_command_even_with_bundled_skill(tmp_path): + matches = find_auto_triggered_skills( + "please match me", + [_mismatched_command(tmp_path)], + loaded_skill_names=set(), + ) + assert matches == [] + + +def test_find_auto_triggered_skills_ignores_bundled_command_with_project_skill(tmp_path): + matches = find_auto_triggered_skills( + "please match me", + [_bundled_command_with_project_skill(tmp_path)], + loaded_skill_names=set(), + ) + assert matches == [] + + +def test_find_auto_triggered_skills_respects_loaded_set(tmp_path): + matches = find_auto_triggered_skills("please match me", [_command(tmp_path)], loaded_skill_names={"demo"}) + assert matches == [] + + +def test_find_auto_triggered_skills_marks_context_skill_tag_as_loaded(tmp_path): + loaded_skill_names: set[str] = set() + + matches = find_auto_triggered_skills( + "please match me", + [_command(tmp_path)], + loaded_skill_names=loaded_skill_names, + context_messages=[Message(role="user", content="demo\n\nDemo prompt")], + ) + + assert matches == [] + assert loaded_skill_names == {"demo"} + + +def test_find_auto_triggered_skills_respects_script_switch(tmp_path): + command = _command(tmp_path) + (tmp_path / "auto_trigger.py").write_text( + "ENABLE_AUTO_TRIGGER = False\ndef should_trigger(prompt):\n return True\n", + encoding="utf-8", + ) + matches = find_auto_triggered_skills("please match me", [command], loaded_skill_names=set()) + assert matches == [] + + +def test_find_auto_triggered_skills_rejects_script_path_escape(tmp_path): + outside_script = tmp_path / "outside.py" + outside_script.write_text( + "ENABLE_AUTO_TRIGGER = True\ndef should_trigger(prompt):\n return True\n", + encoding="utf-8", + ) + skill_root = tmp_path / "skill" + skill_root.mkdir() + fm = SkillFrontmatter(description="demo", auto_trigger={"script": "../outside.py"}) + skill = SkillDefinition( + name="demo", + description="demo", + frontmatter=fm, + content="Demo prompt", + source=SkillSource.BUNDLED, + skill_root=str(skill_root), + ) + command = PromptCommand(name="demo", description="demo", skill=skill, source=SkillSource.BUNDLED) + + matches = find_auto_triggered_skills("please match me", [command], loaded_skill_names=set()) + + assert matches == [] + + +@pytest.mark.asyncio +async def test_process_auto_triggered_skills_returns_processed_results(tmp_path): + from iac_code.skills.auto_trigger import process_auto_triggered_skills + + results = await process_auto_triggered_skills("please match me", [_command(tmp_path)], loaded_skill_names=set()) + assert len(results) == 1 + assert results[0].skill_name == "demo" + assert "demo" in results[0].new_messages[0]["content"] + + +def test_iac_aliyun_trigger_matches_clear_terraform_prompt(): + from iac_code.skills.bundled.iac_aliyun.auto_trigger import should_trigger + + assert should_trigger("生成 terraform 模板,在阿里云上创建 VPC、VSwitch、ECS 和安全组") + + +def test_iac_aliyun_trigger_matches_issue_53_terraform_prompt_variant(): + from iac_code.skills.bundled.iac_aliyun.auto_trigger import should_trigger + + assert should_trigger("生成terraform模版,在阿里云上,region为cn-beijing.") + + +def test_iac_aliyun_trigger_matches_ros_template_prompt(): + from iac_code.skills.bundled.iac_aliyun.auto_trigger import should_trigger + + assert should_trigger("解释这个 ROS 模板里的 ALIYUN::ECS::Instance") + + +def test_iac_aliyun_trigger_matches_alicloud_provider_prompt(): + from iac_code.skills.bundled.iac_aliyun.auto_trigger import should_trigger + + assert should_trigger('用 provider "alicloud" 写一个 ECS 安全组模板') + + +@pytest.mark.parametrize( + "prompt", + [ + "部署这个阿里云 ROS 模板为资源栈", + "把阿里云资源栈模板部署到华东 1", + ], +) +def test_iac_aliyun_trigger_matches_chinese_iac_deployment(prompt): + from iac_code.skills.bundled.iac_aliyun.auto_trigger import should_trigger + + assert should_trigger(prompt) + + +@pytest.mark.parametrize( + "prompt", + [ + "Create an Alibaba Cloud ROS template for an ECS instance", + "生成一个阿里云 ROS 模板,用于创建 ECS 实例", + "Genera una plantilla ROS de Alibaba Cloud para una instancia ECS", + "Crée un modèle ROS Alibaba Cloud pour une instance ECS", + "Erstelle eine Alibaba Cloud ROS-Vorlage für eine ECS-Instanz", + "Alibaba Cloud の ECS インスタンス用の ROS テンプレートを生成して", + "Gere um modelo ROS do Alibaba Cloud para uma instância ECS", + ], +) +def test_iac_aliyun_trigger_matches_supported_languages(prompt): + from iac_code.skills.bundled.iac_aliyun.auto_trigger import should_trigger + + assert should_trigger(prompt) + + +@pytest.mark.parametrize( + "prompt", + [ + "Terraform 是什么?", + "帮我写 AWS Terraform 创建 S3", + "阿里云 ECS 价格怎么样?", + "阿里云 ECS 部署失败了,帮我排查 SSH 登录", + "ROS 机器人导航怎么做?", + ], +) +def test_iac_aliyun_trigger_rejects_non_iac_prompts(prompt): + from iac_code.skills.bundled.iac_aliyun.auto_trigger import should_trigger + + assert not should_trigger(prompt) diff --git a/tests/skills/test_bundled.py b/tests/skills/test_bundled.py index a72d9ca..9f6310a 100644 --- a/tests/skills/test_bundled.py +++ b/tests/skills/test_bundled.py @@ -14,6 +14,10 @@ def setup_method(self): """Clear bundled skills before each test.""" _bundled_skills.clear() + def teardown_method(self): + """Avoid leaking test-only bundled skills into later test modules.""" + _bundled_skills.clear() + def test_register_static_skill(self): register_bundled_skill( name="test", @@ -72,3 +76,13 @@ def test_skill_frontmatter_populated(self): assert skill.frontmatter.effort == "high" assert skill.frontmatter.context == "fork" assert skill.frontmatter.agent == "explore" + + def test_register_static_skill_with_auto_trigger_metadata(self): + register_bundled_skill( + name="test", + description="A test skill", + prompt="Do something.", + auto_trigger={"script": "auto_trigger.py"}, + ) + skill = get_bundled_skills()[0] + assert skill.auto_trigger == {"script": "auto_trigger.py"} diff --git a/tests/skills/test_frontmatter.py b/tests/skills/test_frontmatter.py index 6213e2c..a786762 100644 --- a/tests/skills/test_frontmatter.py +++ b/tests/skills/test_frontmatter.py @@ -139,3 +139,12 @@ def test_data_to_frontmatter_accepts_scalar_allowed_tools(self): assert fm.allowed_tools == ["bash"] assert fm.arguments == ["repo"] assert fm.paths == ["src"] + + def test_auto_trigger_metadata_mapping(self): + md = "---\nauto_trigger:\n script: auto_trigger.py\n---\nBody" + fm, _ = parse_frontmatter(md) + assert fm.auto_trigger == {"script": "auto_trigger.py"} + + def test_auto_trigger_metadata_ignores_non_mapping(self): + fm = _data_to_frontmatter({"auto_trigger": "auto_trigger.py"}) + assert fm.auto_trigger == {} diff --git a/tests/skills/test_listing.py b/tests/skills/test_listing.py index c0cdaf5..3e75827 100644 --- a/tests/skills/test_listing.py +++ b/tests/skills/test_listing.py @@ -1,6 +1,8 @@ """Tests for skill listing generation.""" from iac_code.commands.registry import PromptCommand +from iac_code.skills.bundled import get_bundled_skills, init_bundled_skills +from iac_code.skills.discovery import skill_to_command from iac_code.skills.frontmatter import SkillFrontmatter from iac_code.skills.listing import _assemble, _format_full, _format_truncated, build_skill_listing, get_char_budget from iac_code.skills.skill_definition import SkillDefinition @@ -87,3 +89,13 @@ def test_assemble_adds_header(self): result = _assemble(["- test: description"]) assert result.startswith("The following skills are available") assert result.endswith("- test: description") + + def test_iac_aliyun_listing_contains_strong_trigger_guidance(self): + init_bundled_skills() + skill = next(s for s in get_bundled_skills() if s.name == "iac-aliyun") + result = build_skill_listing([skill_to_command(skill)]) + assert "iac-aliyun" in result + assert "Alibaba Cloud" in result + assert "Terraform" in result + assert "alicloud provider" in result + assert "必须先调用 skill 工具加载 iac-aliyun" in result diff --git a/tests/test_config.py b/tests/test_config.py index ac234a8..dd420e4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,8 +2,11 @@ from __future__ import annotations +import sys from unittest.mock import MagicMock, patch +import pytest + from iac_code.config import PARTNER_SOURCES, PartnerSource, get_available_partner_sources @@ -222,6 +225,18 @@ def test_save_yaml_writes_file(self, tmp_path): content = yaml.safe_load(path.read_text()) assert content == {"key": "value", "num": 42} + @pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes are not meaningful on Windows") + def test_save_yaml_writes_owner_only_file(self, tmp_path): + """_save_yaml restricts parent directory and written file.""" + path = tmp_path / "private" / "settings.yml" + + from iac_code.config import _save_yaml + + _save_yaml(path, {"secret": "value"}) + + assert oct(path.parent.stat().st_mode & 0o777) == "0o700" + assert oct(path.stat().st_mode & 0o777) == "0o600" + def test_save_yaml_creates_parent_dirs(self, tmp_path): """_save_yaml creates parent directories if they don't exist.""" path = tmp_path / "subdir" / "nested" / "test.yml" diff --git a/tests/test_config_dir_env.py b/tests/test_config_dir_env.py index 2866edd..ca1e8f8 100644 --- a/tests/test_config_dir_env.py +++ b/tests/test_config_dir_env.py @@ -2,9 +2,12 @@ from __future__ import annotations +import sys from pathlib import Path from unittest.mock import patch +import pytest + class TestResolveConfigDirFallback: def test_unset_falls_back_to_home_dot_iac_code(self, monkeypatch, tmp_path): @@ -39,6 +42,16 @@ def test_absolute_path_used_as_is(self, monkeypatch, tmp_path): assert result == target.resolve() assert result.is_dir() + @pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes are not meaningful on Windows") + def test_get_config_dir_is_owner_only(self, monkeypatch, tmp_path): + target = tmp_path / "custom-config" + monkeypatch.setenv("IAC_CODE_CONFIG_DIR", str(target)) + from iac_code.config import get_config_dir + + result = get_config_dir() + + assert oct(result.stat().st_mode & 0o777) == "0o700" + class TestResolveConfigDirExpansion: def test_tilde_expansion(self, monkeypatch, tmp_path): diff --git a/tests/test_config_env_overrides.py b/tests/test_config_env_overrides.py index 682a4f1..3763a33 100644 --- a/tests/test_config_env_overrides.py +++ b/tests/test_config_env_overrides.py @@ -71,6 +71,42 @@ def test_provider_with_surrounding_whitespace_accepted(self, monkeypatch): assert _get_env_overrides()["provider_key"] == "dashscope" + @pytest.mark.parametrize( + ("value", "expected"), + [ + ("OpenAPI Compatible", "openapi_compatible"), + ("openapi-compatible", "openapi_compatible"), + ("openapi_compatible", "openapi_compatible"), + ("oPeNaPi CoMpAtIbLe", "openapi_compatible"), + ("DashScope Token Plan", "dashscope_token_plan"), + ("dashscope-token-plan", "dashscope_token_plan"), + ("dashscope_token_plan", "dashscope_token_plan"), + ], + ) + def test_provider_env_accepts_normalized_display_names_and_keys(self, monkeypatch, value, expected): + monkeypatch.setenv("IAC_CODE_PROVIDER", value) + from iac_code.config import _get_env_overrides + + assert _get_env_overrides()["provider_key"] == expected + + def test_provider_name_lookup_rejects_colliding_normalized_aliases(self, monkeypatch): + from types import SimpleNamespace + + import iac_code.providers.registry as registry + from iac_code.config import _build_provider_name_to_key + + monkeypatch.setattr( + registry, + "PROVIDER_REGISTRY", + { + "foo_bar": SimpleNamespace(key="foo_bar", name="Foo Bar"), + "foobar": SimpleNamespace(key="foobar", name="Foobar"), + }, + ) + + with pytest.raises(ValueError, match="Ambiguous provider alias"): + _build_provider_name_to_key() + def test_provider_invalid_raises_with_canonical_names(self, monkeypatch): monkeypatch.setenv("IAC_CODE_PROVIDER", "Unknown") from iac_code.config import _get_env_overrides @@ -81,6 +117,17 @@ def test_provider_invalid_raises_with_canonical_names(self, monkeypatch): for canonical in ("Anthropic", "OpenAI", "DashScope", "DeepSeek"): assert canonical in msg + def test_provider_invalid_error_is_translatable(self, monkeypatch): + import iac_code.config as config + + monkeypatch.setenv("IAC_CODE_PROVIDER", "Unknown") + monkeypatch.setattr(config, "_", lambda msg: f"TRANSLATED:{msg}", raising=False) + + with pytest.raises(ValueError) as exc: + config._get_env_overrides() + + assert str(exc.value).startswith("TRANSLATED:Invalid IAC_CODE_PROVIDER value") + def test_all_four_env_vars_returned(self, monkeypatch): monkeypatch.setenv("IAC_CODE_PROVIDER", "DeepSeek") monkeypatch.setenv("IAC_CODE_MODEL", "deepseek-v4-pro") diff --git a/tests/test_services/test_telemetry/test_fallback.py b/tests/test_services/test_telemetry/test_fallback.py index 02c1010..bc7a50e 100644 --- a/tests/test_services/test_telemetry/test_fallback.py +++ b/tests/test_services/test_telemetry/test_fallback.py @@ -1,6 +1,7 @@ """Tests for the FallbackStore class.""" import json +import sys import pytest @@ -20,6 +21,14 @@ def test_write_creates_jsonl_file(store, tmp_path): assert path.suffix == ".jsonl" +@pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes are not meaningful on Windows") +def test_write_creates_owner_only_file(store): + path = store.write("iac_sess_private", [{"event.name": "iac.test", "k": 1}]) + + assert oct(path.parent.stat().st_mode & 0o777) == "0o700" + assert oct(path.stat().st_mode & 0o777) == "0o600" + + def test_write_produces_one_line_per_event(store): events = [{"event.name": f"iac.n{i}"} for i in range(3)] path = store.write("iac_sess_abc", events) diff --git a/tests/tools/test_result_storage.py b/tests/tools/test_result_storage.py index 859cfd2..b1d34c4 100644 --- a/tests/tools/test_result_storage.py +++ b/tests/tools/test_result_storage.py @@ -1,4 +1,6 @@ import os +import sys +from pathlib import Path import pytest @@ -24,6 +26,39 @@ def test_large_result_externalized(self, storage): assert result.file_path is not None assert os.path.exists(result.file_path) + @pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes are not meaningful on Windows") + def test_externalized_file_is_owner_only(self, tmp_path): + storage = ResultStorage(storage_dir=str(tmp_path / "tool-results"), max_inline_chars=1) + + result = storage.process(tool_use_id="private", content="long output") + + file_path = Path(result.file_path) + assert oct(file_path.parent.stat().st_mode & 0o777) == "0o700" + assert oct(file_path.stat().st_mode & 0o777) == "0o600" + + @pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes are not meaningful on Windows") + def test_externalized_file_restricts_tool_results_root_and_session_dir(self, tmp_path): + storage = ResultStorage(storage_dir=str(tmp_path / "tool-results" / "session-1"), max_inline_chars=1) + + result = storage.process(tool_use_id="private", content="long output") + + file_path = Path(result.file_path) + assert oct((tmp_path / "tool-results").stat().st_mode & 0o777) == "0o700" + assert oct(file_path.parent.stat().st_mode & 0o777) == "0o700" + + @pytest.mark.parametrize("tool_use_id", ["../escape", "a/b", r"a\b", "/tmp/escape", "", "."]) + def test_externalized_file_cannot_escape_storage_dir(self, tmp_path, tool_use_id): + storage_dir = tmp_path / "tool-results" + storage = ResultStorage(storage_dir=str(storage_dir), max_inline_chars=1) + + result = storage.process(tool_use_id=tool_use_id, content="long output") + + assert result.file_path is not None + file_path = Path(result.file_path) + assert file_path.parent == storage_dir + assert not (tmp_path / "escape.txt").exists() + assert file_path.name.endswith(".txt") + def test_externalized_file_content(self, storage): content = "line\n" * 100 result = storage.process(tool_use_id="t3", content=content) diff --git a/tests/ui/core/test_input_history.py b/tests/ui/core/test_input_history.py index 41749b4..25a87a9 100644 --- a/tests/ui/core/test_input_history.py +++ b/tests/ui/core/test_input_history.py @@ -1,5 +1,10 @@ """Tests for InputHistory.""" +import json +import sys + +import pytest + from iac_code.ui.core.input_history import InputHistory @@ -57,6 +62,18 @@ def test_empty_search_returns_all(self, tmp_path): assert "alpha" in results assert "beta" in results + def test_entries_returns_oldest_first_copy(self, tmp_path): + history_file = str(tmp_path / "history.txt") + h = InputHistory(history_file) + h.append("first") + h.append("second") + + entries = h.entries() + entries.append("mutated") + + assert entries == ["first", "second", "mutated"] + assert h.entries() == ["first", "second"] + def test_search_no_match(self, tmp_path): history_file = str(tmp_path / "history.txt") h = InputHistory(history_file) @@ -141,6 +158,15 @@ def test_file_created_on_first_append(self, tmp_path): h.append("test") assert (tmp_path / "history.txt").exists() + @pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes are not meaningful on Windows") + def test_history_file_is_owner_only(self, tmp_path): + history_file = tmp_path / "history.txt" + h = InputHistory(str(history_file)) + + h.append("test") + + assert oct(history_file.stat().st_mode & 0o777) == "0o600" + def test_empty_entry_not_appended(self, tmp_path): history_file = str(tmp_path / "history.txt") h = InputHistory(history_file) @@ -199,3 +225,83 @@ def test_navigate_after_session_only_append(self, tmp_path): assert result == "/auth login" result2 = h.navigate(-1) assert result2 == "first" + + def test_multiline_entry_persists_as_single_jsonl_entry(self, tmp_path): + history_file = tmp_path / "history.txt" + entry = "line1\nline2" + + h1 = InputHistory(str(history_file)) + h1.append(entry) + + raw_lines = history_file.read_text(encoding="utf-8").splitlines() + assert len(raw_lines) == 1 + assert json.loads(raw_lines[0]) == { + "format": "iac-code-input-history-v1", + "text": entry, + } + + h2 = InputHistory(str(history_file)) + assert h2.search("line1") == [entry] + assert h2.navigate(-1) == entry + + def test_multiline_entry_dedupes_consecutive_duplicates(self, tmp_path): + history_file = str(tmp_path / "history.txt") + entry = "line1\nline2" + h = InputHistory(history_file) + + h.append(entry) + h.append(entry) + + assert h.search("line1") == [entry] + + def test_multiline_entry_keeps_non_consecutive_duplicates(self, tmp_path): + history_file = str(tmp_path / "history.txt") + entry = "line1\nline2" + h = InputHistory(history_file) + + h.append(entry) + h.append("other") + h.append(entry) + + assert h.search("line1") == [entry, entry] + + def test_multiline_session_only_entry_not_saved_to_disk(self, tmp_path): + history_file = str(tmp_path / "history.txt") + entry = "line1\nline2" + h = InputHistory(history_file) + + h.append("persisted") + h.append(entry, persist=False) + + assert h.navigate(-1) == entry + h2 = InputHistory(history_file) + assert h2.search("line1") == [] + assert h2.search("persisted") == ["persisted"] + + def test_legacy_plain_line_history_still_loads(self, tmp_path): + history_file = tmp_path / "history.txt" + history_file.write_text("old one\nold two\n", encoding="utf-8") + + h = InputHistory(str(history_file)) + + assert h.search("old") == ["old two", "old one"] + + def test_malformed_jsonl_line_loads_as_legacy_entry(self, tmp_path): + history_file = tmp_path / "history.txt" + history_file.write_text( + '{"format": "iac-code-input-history-v1", "text": "ok"}\n{"text": 123}\n[1, 2]\n{broken\n', + encoding="utf-8", + ) + + h = InputHistory(str(history_file)) + + assert h.search("") == ["{broken", "[1, 2]", '{"text": 123}', "ok"] + + def test_legacy_json_text_line_stays_literal(self, tmp_path): + history_file = tmp_path / "history.txt" + legacy_entry = '{"text": "keep literal"}' + history_file.write_text(legacy_entry + "\n", encoding="utf-8") + + h = InputHistory(str(history_file)) + + assert h.search("") == [legacy_entry] diff --git a/tests/ui/core/test_prompt_input.py b/tests/ui/core/test_prompt_input.py index fbb6f24..ba70caf 100644 --- a/tests/ui/core/test_prompt_input.py +++ b/tests/ui/core/test_prompt_input.py @@ -180,6 +180,28 @@ def test_multiline_esc_enter(self): assert "\n" in inp._get_text() assert inp._submitted is False + def test_shift_enter_inserts_newline(self): + inp = make_input() + inp._handle_key(_key("a")) + inp._handle_key(KeyEvent(key="enter", char="", shift=True)) + + assert inp._get_text() == "a\n" + assert inp._submitted is False + + def test_shift_enter_with_suggestions_inserts_newline_without_accepting(self): + from unittest.mock import MagicMock + + aggregator = MagicMock() + aggregator.suggestions = [MagicMock()] + aggregator.accept_selected.return_value = ("/model ", 0, 4) + inp = make_input(suggestion_aggregator=aggregator) + + inp._handle_key(KeyEvent(key="enter", char="", shift=True)) + + assert inp._get_text() == "\n" + assert inp._submitted is False + aggregator.accept_selected.assert_not_called() + def test_enter_submits(self): inp = make_input() for ch in "hello": @@ -228,6 +250,17 @@ def test_history_navigate_up(self, tmp_path): inp._handle_key(_key("up")) assert inp._get_text() == "prev command" + def test_history_navigate_multiline_entry_sets_full_buffer(self, tmp_path): + from iac_code.ui.core.input_history import InputHistory + + history = InputHistory(str(tmp_path / "hist.txt")) + history.append("line1\nline2") + inp = make_input(history=history) + + inp._handle_key(_key("up")) + + assert inp._get_text() == "line1\nline2" + def test_history_navigate_down(self, tmp_path): """Down arrow after navigating up restores None (clears buffer signal).""" from iac_code.ui.core.input_history import InputHistory @@ -332,6 +365,32 @@ def test_escape_with_suggestions_dismisses_aggregator(self): assert aggregator.dismissed is True +class TestPromptInputInsertText: + def test_insert_text_at_cursor(self): + inp = make_input() + for ch in "hello": + inp._handle_key(_key(ch)) + inp._handle_key(_key("left")) + inp._handle_key(_key("left")) + + inp.insert_text(" brave") + + assert inp._get_text() == "hel bravelo" + assert inp._cursor == len("hel brave") + assert inp._text_changed is True + + def test_insert_empty_text_does_not_mark_changed(self): + inp = make_input() + inp._handle_key(_key("h")) + inp._text_changed = False + + inp.insert_text("") + + assert inp._get_text() == "h" + assert inp._cursor == 1 + assert inp._text_changed is False + + class TestPromptInputHelpers: def test_update_suggestions_sync_uses_current_buffer_and_cursor(self): aggregator = DummyAggregator() diff --git a/tests/ui/core/test_raw_input.py b/tests/ui/core/test_raw_input.py index 8ac35d8..1993fce 100644 --- a/tests/ui/core/test_raw_input.py +++ b/tests/ui/core/test_raw_input.py @@ -164,6 +164,36 @@ def test_unknown_sequence(self): event = RawInputCapture._parse_escape_sequence("[999~") assert event.key == "unknown" + def test_shift_enter_csi_u(self): + event = RawInputCapture._parse_escape_sequence("[13;2u") + assert event.key == "enter" + assert event.shift is True + assert event.key_id == "enter" + + def test_shift_enter_xterm_modify_other_keys(self): + event = RawInputCapture._parse_escape_sequence("[27;2;13~") + assert event.key == "enter" + assert event.shift is True + + def test_shift_enter_modified_special_key(self): + event = RawInputCapture._parse_escape_sequence("[13;2~") + assert event.key == "enter" + assert event.shift is True + + def test_unknown_modified_enter_csi_u(self): + event = RawInputCapture._parse_escape_sequence("[13;5u") + assert event.key == "unknown" + + def test_ctrl_c_xterm_modify_other_keys(self): + event = RawInputCapture._parse_escape_sequence("[27;5;99~") + assert event.key == "c" + assert event.ctrl is True + + def test_ctrl_r_csi_u(self): + event = RawInputCapture._parse_escape_sequence("[114;5u") + assert event.key == "r" + assert event.ctrl is True + def test_mouse_wheel_up(self): # SGR mouse encoding: button 64 = wheel up. event = RawInputCapture._parse_escape_sequence("[<64;10;5M") @@ -209,6 +239,10 @@ def test_enter_and_exit_toggle_terminal_modes(self, monkeypatch): ("setraw", 7), (7, b"\033[?2004h"), (7, b"\033[?1004h"), + (7, b"\033[>1u"), + (7, b"\033[>4;2m"), + (7, b"\033[>4;0m"), + (7, b"\033[ None: + self.messages: list[tuple[str, str]] = [] + self.recorded_turns: list[str] = [] + self.permission_allowed = permission_allowed + self.permission_events = [] + + def print_system_message(self, text: str, style: str = "yellow") -> None: + self.messages.append((text, style)) + + def record_user_turn(self, text: str) -> None: + self.recorded_turns.append(text) + + async def prompt_permission(self, event) -> bool: + self.permission_events.append(event) + return self.permission_allowed + + +class RecordingHistory: + def __init__(self) -> None: + self.appended: list[str] = [] + + def append(self, entry: str) -> None: + self.appended.append(entry) + + def reset_navigation(self) -> None: + pass + + +class RecordingContextManager: + def __init__(self) -> None: + self.user_messages: list[str] = [] + self.assistant_messages: list[str] = [] + + def add_user_message(self, message: str) -> None: + self.user_messages.append(message) + + def add_assistant_message(self, message: str) -> None: + self.assistant_messages.append(message) + + +class FakeBashTool(Tool): + def __init__(self, result: ToolResult, permission: PermissionResult | None = None) -> None: + self.result = result + self.permission = permission or PermissionResult(behavior="allow") + self.calls: list[tuple[dict, str]] = [] + + @property + def name(self) -> str: + return "bash" + + @property + def description(self) -> str: + return "Fake bash" + + @property + def input_schema(self) -> dict: + return { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + } + + async def check_permissions(self, input: dict, context=None) -> PermissionResult: + return self.permission + + async def execute(self, *, tool_input: dict, context) -> ToolResult: + self.calls.append((tool_input, context.cwd)) + return self.result + + +def make_repl( + tool: FakeBashTool | None, + cwd: str, + *, + permission_context: ToolPermissionContext | None = None, + permission_allowed: bool = True, +) -> InlineREPL: + repl = InlineREPL.__new__(InlineREPL) + repl._original_cwd = cwd + repl.renderer = FakeRenderer(permission_allowed=permission_allowed) + repl._history = RecordingHistory() + repl._agent_loop = SimpleNamespace(context_manager=RecordingContextManager()) + repl.store = SimpleNamespace(get_state=lambda: SimpleNamespace(permission_context=permission_context)) + repl.tool_registry = SimpleNamespace(get=lambda name: tool if name == "bash" else None) + return repl + + +@pytest.mark.asyncio +async def test_shell_escape_executes_registered_bash_tool(tmp_path): + tool = FakeBashTool(ToolResult.success("STDOUT:\nhello\nExit code: 0")) + repl = make_repl(tool, str(tmp_path)) + + await repl._handle_shell_escape("!echo hello") + + assert tool.calls == [({"command": "echo hello"}, str(tmp_path))] + assert ("$ echo hello", "dim") in repl.renderer.messages + assert ("STDOUT:\nhello\nExit code: 0", "white") in repl.renderer.messages + assert repl.renderer.recorded_turns == [] + assert repl._history.appended == [] + assert repl._agent_loop.context_manager.user_messages == [] + assert repl._agent_loop.context_manager.assistant_messages == [] + + +@pytest.mark.asyncio +async def test_shell_escape_uses_tool_executor_not_direct_tool_execute(tmp_path, monkeypatch): + tool = FakeBashTool(ToolResult.error("direct execution should not run")) + repl = make_repl(tool, str(tmp_path)) + captured = {} + + async def fake_execute_batch(self, calls, context): + captured["calls"] = calls + captured["cwd"] = context.cwd + return [ToolResult.success("executor output")] + + monkeypatch.setattr("iac_code.tools.tool_executor.ToolExecutor.execute_batch", fake_execute_batch) + + await repl._handle_shell_escape("!echo via executor") + + assert tool.calls == [] + assert captured["cwd"] == str(tmp_path) + call = captured["calls"][0] + assert call.id == "shell-escape" + assert call.name == "bash" + assert call.input == {"command": "echo via executor"} + assert ("executor output", "white") in repl.renderer.messages + + +@pytest.mark.asyncio +async def test_shell_escape_empty_command_prints_usage_without_execution(tmp_path): + tool = FakeBashTool(ToolResult.success("unused")) + repl = make_repl(tool, str(tmp_path)) + + await repl._handle_shell_escape("! ") + + assert tool.calls == [] + assert repl.renderer.messages == [("Usage: !", "yellow")] + + +@pytest.mark.asyncio +async def test_shell_escape_error_result_prints_red_output(tmp_path): + tool = FakeBashTool(ToolResult.error("STDERR:\nnot found\nExit code: 127")) + repl = make_repl(tool, str(tmp_path)) + + await repl._handle_shell_escape("!missing-command") + + assert tool.calls == [({"command": "missing-command"}, str(tmp_path))] + assert ("STDERR:\nnot found\nExit code: 127", "red") in repl.renderer.messages + + +@pytest.mark.asyncio +async def test_shell_escape_missing_bash_tool_prints_error(tmp_path): + repl = make_repl(None, str(tmp_path)) + + await repl._handle_shell_escape("!echo hello") + + assert repl.renderer.messages == [("Shell command support is unavailable.", "red")] + + +@pytest.mark.asyncio +async def test_shell_escape_permission_deny_does_not_execute(tmp_path): + tool = FakeBashTool(ToolResult.success("unused"), PermissionResult(behavior="deny", message="blocked")) + repl = make_repl(tool, str(tmp_path), permission_context=ToolPermissionContext(cwd=str(tmp_path))) + + await repl._handle_shell_escape("!mkdir blocked") + + assert tool.calls == [] + assert repl.renderer.messages == [("blocked", "red")] + + +@pytest.mark.asyncio +async def test_shell_escape_permission_prompt_rejection_does_not_execute(tmp_path): + tool = FakeBashTool(ToolResult.success("unused"), PermissionResult(behavior="ask", message="confirm")) + repl = make_repl(tool, str(tmp_path), permission_allowed=False) + + await repl._handle_shell_escape("!mkdir maybe") + + assert tool.calls == [] + assert repl.renderer.messages == [("Permission denied.", "red")] + assert [event.tool_input for event in repl.renderer.permission_events] == [{"command": "mkdir maybe"}] + + +@pytest.mark.asyncio +async def test_interactive_shell_escape_resets_history_navigation_without_appending(tmp_path): + history = InputHistory(str(tmp_path / "history")) + history.append("previous prompt") + assert history.navigate(-1, current_input="draft") == "previous prompt" + assert history.is_navigating is True + + repl = InlineREPL.__new__(InlineREPL) + repl._history = history + handled: list[str] = [] + + async def handle_shell_escape(user_input: str) -> None: + handled.append(user_input) + + repl._handle_shell_escape = handle_shell_escape + + await repl._handle_interactive_shell_escape("!echo hello") + + assert handled == ["!echo hello"] + assert history.is_navigating is False + assert history.search("") == ["previous prompt"] diff --git a/tests/utils/image/test_store.py b/tests/utils/image/test_store.py index 2e758fe..277989e 100644 --- a/tests/utils/image/test_store.py +++ b/tests/utils/image/test_store.py @@ -1,3 +1,4 @@ +import sys from pathlib import Path import pytest @@ -23,6 +24,21 @@ def test_store_writes_per_session_file_with_0o600(tmp_path, monkeypatch): assert stat.S_IMODE(p.stat().st_mode) == 0o600 +@pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes are not meaningful on Windows") +def test_store_directories_are_owner_only(tmp_path, monkeypatch): + monkeypatch.setattr("iac_code.utils.image.store._get_base_dir", lambda: tmp_path / "image-cache") + store = ImageStore(session_id="sess-a") + pc = PastedContent(id=7, type="image", content="aGVsbG8=", media_type="image/png") + + path = store.store(pc) + + assert path is not None + base_dir = tmp_path / "image-cache" + session_dir = base_dir / "sess-a" + assert oct(base_dir.stat().st_mode & 0o777) == "0o700" + assert oct(session_dir.stat().st_mode & 0o777) == "0o700" + + def test_lru_eviction_cap(tmp_path, monkeypatch): monkeypatch.setattr("iac_code.utils.image.store._get_base_dir", lambda: tmp_path / "image-cache") monkeypatch.setattr("iac_code.utils.image.store.MAX_STORED_IMAGE_PATHS", 3) diff --git a/tests/utils/test_file_security.py b/tests/utils/test_file_security.py index abe07a5..cf7c70b 100644 --- a/tests/utils/test_file_security.py +++ b/tests/utils/test_file_security.py @@ -29,6 +29,32 @@ def test_unix_directory_permissions(tmp_path): assert oct(d.stat().st_mode & 0o777) == "0o700" +@pytest.mark.skipif(sys.platform == "win32", reason="chmod has no effect on Windows NTFS") +def test_ensure_private_dir_creates_and_restricts(tmp_path): + from iac_code.utils.file_security import ensure_private_dir + + path = tmp_path / "a" / "private" + with patch("iac_code.utils.file_security._IS_WINDOWS", False): + result = ensure_private_dir(path) + + assert result == path + assert path.is_dir() + assert oct(path.stat().st_mode & 0o777) == "0o700" + + +@pytest.mark.skipif(sys.platform == "win32", reason="chmod has no effect on Windows NTFS") +def test_ensure_private_file_restricts_existing_file(tmp_path): + from iac_code.utils.file_security import ensure_private_file + + path = tmp_path / "secret.txt" + path.write_text("secret", encoding="utf-8") + with patch("iac_code.utils.file_security._IS_WINDOWS", False): + result = ensure_private_file(path) + + assert result == path + assert oct(path.stat().st_mode & 0o777) == "0o600" + + def test_windows_file_calls_icacls(tmp_path): f = tmp_path / "secret.txt" f.write_text("data") diff --git a/tests/utils/test_log_symlink.py b/tests/utils/test_log_symlink.py index b26af32..87cd3e5 100644 --- a/tests/utils/test_log_symlink.py +++ b/tests/utils/test_log_symlink.py @@ -1,7 +1,9 @@ """Tests for symlink fallback in iac_code.utils.log.""" +import sys from unittest.mock import patch +import pytest from loguru import logger from iac_code.utils.log import enable_debug_at_runtime, setup_logging @@ -23,6 +25,33 @@ def test_setup_logging_symlink_oserror_falls_back_to_copy(tmp_path, monkeypatch) assert not latest.is_symlink() +@pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes are not meaningful on Windows") +def test_setup_logging_files_are_owner_only(tmp_path, monkeypatch): + monkeypatch.setattr("iac_code.utils.log.get_config_dir", lambda: tmp_path) + logger.remove() + + setup_logging(session_id="private", debug=False) + + log_dir = tmp_path / "logs" + log_file = log_dir / "private.log" + assert oct(log_dir.stat().st_mode & 0o777) == "0o700" + assert oct(log_file.stat().st_mode & 0o777) == "0o600" + + +@pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes are not meaningful on Windows") +def test_setup_logging_copy_fallback_latest_is_owner_only(tmp_path, monkeypatch): + monkeypatch.setattr("iac_code.utils.log.get_config_dir", lambda: tmp_path) + logger.remove() + + with patch("iac_code.utils.log.Path.symlink_to", side_effect=OSError("WinError 1314")): + setup_logging(session_id="private-fallback", debug=False) + + latest = tmp_path / "logs" / "latest.log" + assert latest.exists() + assert not latest.is_symlink() + assert oct(latest.stat().st_mode & 0o777) == "0o600" + + def test_enable_debug_symlink_oserror_falls_back(tmp_path, monkeypatch): """When symlink_to raises OSError, enable_debug_at_runtime should not crash.""" monkeypatch.setattr("iac_code.utils.log.get_config_dir", lambda: tmp_path) diff --git a/uv.lock b/uv.lock index 7549f81..1b68896 100644 --- a/uv.lock +++ b/uv.lock @@ -1331,8 +1331,8 @@ requires-dist = [ { name = "starlette", marker = "extra == 'a2a'", specifier = ">=0.39.0" }, { name = "starlette", marker = "extra == 'http'", specifier = ">=0.39.0" }, { name = "tiktoken", specifier = ">=0.7.0" }, - { name = "tree-sitter", specifier = ">=0.23" }, - { name = "tree-sitter-bash", specifier = ">=0.23" }, + { name = "tree-sitter", specifier = ">=0.25,<0.26" }, + { name = "tree-sitter-bash", specifier = ">=0.25,<0.26" }, { name = "typer", specifier = ">=0.9.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'a2a'", specifier = ">=0.30.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'http'", specifier = ">=0.30.0" }, diff --git a/website/docs/cli/interactive-mode.md b/website/docs/cli/interactive-mode.md index 4dfc8bd..c1e536b 100644 --- a/website/docs/cli/interactive-mode.md +++ b/website/docs/cli/interactive-mode.md @@ -24,3 +24,26 @@ Then describe what you want to build: ```text Create a VPC, two ECS instances, and a security group that allows SSH from my office IP. ``` + +## Editing input + +Use `Shift+Enter` to insert a newline without sending the prompt. Press `Enter` +on its own to submit the complete prompt. + +If your terminal does not report `Shift+Enter` separately, press `Esc` and then +`Enter` to insert a newline. Multi-line prompts are saved as one history entry, +so pressing `Up` restores the full prompt. + +## Shell escapes + +Prefix a line with `!` to run a local shell command from the REPL through the +built-in `bash` tool: + +```text +!pwd +!git status --short +``` + +IaC Code applies the normal tool permission checks, runs the command in the +current project context, and prints the output in the terminal. The command is +not sent to the model as a chat message. diff --git a/website/i18n/de/docusaurus-plugin-content-docs/current/cli/interactive-mode.md b/website/i18n/de/docusaurus-plugin-content-docs/current/cli/interactive-mode.md index 4d1d994..53255d1 100644 --- a/website/i18n/de/docusaurus-plugin-content-docs/current/cli/interactive-mode.md +++ b/website/i18n/de/docusaurus-plugin-content-docs/current/cli/interactive-mode.md @@ -24,3 +24,20 @@ Beschreiben Sie dann, was Sie erstellen moechten: ```text Create a VPC, two ECS instances, and a security group that allows SSH from my office IP. ``` + +## Eingabe bearbeiten + +Verwenden Sie `Shift+Enter`, um eine neue Zeile einzufuegen, ohne den Prompt zu senden. Druecken Sie normales `Enter`, um den vollstaendigen Prompt zu senden. + +Wenn Ihr Terminal `Shift+Enter` nicht getrennt uebermittelt, druecken Sie `Esc` und dann `Enter`, um eine neue Zeile einzufuegen. Mehrzeilige Prompts werden als ein Verlaufseintrag gespeichert, sodass `Up` den vollstaendigen Prompt wiederherstellt. + +## Shell-Escapes + +Stellen Sie einer Zeile `!` voran, um im REPL einen lokalen Shell-Befehl ueber das integrierte `bash`-Tool auszufuehren: + +```text +!pwd +!git status --short +``` + +IaC Code wendet die normalen Tool-Berechtigungspruefungen an, fuehrt den Befehl im aktuellen Projektkontext aus und zeigt die Ausgabe im Terminal an. Der Befehl wird nicht als Chat-Nachricht an das Modell gesendet. diff --git a/website/i18n/es/docusaurus-plugin-content-docs/current/cli/interactive-mode.md b/website/i18n/es/docusaurus-plugin-content-docs/current/cli/interactive-mode.md index 40c6f3e..02c4f1b 100644 --- a/website/i18n/es/docusaurus-plugin-content-docs/current/cli/interactive-mode.md +++ b/website/i18n/es/docusaurus-plugin-content-docs/current/cli/interactive-mode.md @@ -24,3 +24,20 @@ Luego describe lo que quieres construir: ```text Create a VPC, two ECS instances, and a security group that allows SSH from my office IP. ``` + +## Editar la entrada + +Usa `Shift+Enter` para insertar una nueva linea sin enviar el prompt. Pulsa `Enter` solo para enviar el prompt completo. + +Si tu terminal no informa `Shift+Enter` por separado, pulsa `Esc` y luego `Enter` para insertar una nueva linea. Los prompts de varias lineas se guardan como una sola entrada de historial, por lo que `Up` restaura el prompt completo. + +## Shell escapes + +Antepon `!` a una linea para ejecutar un comando local de shell desde el REPL mediante la herramienta `bash` integrada: + +```text +!pwd +!git status --short +``` + +IaC Code aplica las comprobaciones normales de permisos de herramientas, ejecuta el comando en el contexto del proyecto actual y muestra la salida en el terminal. El comando no se envia al modelo como mensaje de chat. diff --git a/website/i18n/fr/docusaurus-plugin-content-docs/current/cli/interactive-mode.md b/website/i18n/fr/docusaurus-plugin-content-docs/current/cli/interactive-mode.md index 88dfc19..c8f39a2 100644 --- a/website/i18n/fr/docusaurus-plugin-content-docs/current/cli/interactive-mode.md +++ b/website/i18n/fr/docusaurus-plugin-content-docs/current/cli/interactive-mode.md @@ -24,3 +24,20 @@ Puis décrivez ce que vous souhaitez construire : ```text Create a VPC, two ECS instances, and a security group that allows SSH from my office IP. ``` + +## Modifier la saisie + +Utilisez `Shift+Enter` pour insérer une nouvelle ligne sans envoyer le prompt. Appuyez sur `Enter` seul pour envoyer le prompt complet. + +Si votre terminal ne signale pas `Shift+Enter` séparément, appuyez sur `Esc` puis sur `Enter` pour insérer une nouvelle ligne. Les prompts multilignes sont enregistrés comme une seule entrée d'historique, donc `Up` restaure le prompt complet. + +## Shell escapes + +Préfixez une ligne avec `!` pour exécuter une commande shell locale depuis le REPL via l'outil `bash` intégré : + +```text +!pwd +!git status --short +``` + +IaC Code applique les vérifications de permissions d'outil habituelles, exécute la commande dans le contexte du projet actuel et affiche la sortie dans le terminal. La commande n'est pas envoyée au modèle comme message de chat. diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/current/cli/interactive-mode.md b/website/i18n/ja/docusaurus-plugin-content-docs/current/cli/interactive-mode.md index 7fa709c..81031eb 100644 --- a/website/i18n/ja/docusaurus-plugin-content-docs/current/cli/interactive-mode.md +++ b/website/i18n/ja/docusaurus-plugin-content-docs/current/cli/interactive-mode.md @@ -24,3 +24,20 @@ iac-code ```text Create a VPC, two ECS instances, and a security group that allows SSH from my office IP. ``` + +## 入力の編集 + +`Shift+Enter` を使うと、プロンプトを送信せずに改行を挿入できます。完全なプロンプトを送信するには、通常の `Enter` を押します。 + +端末が `Shift+Enter` を個別に通知しない場合は、`Esc` を押してから `Enter` を押すと改行を挿入できます。複数行プロンプトは 1 つの履歴項目として保存されるため、`Up` で完全なプロンプトを復元できます。 + +## Shell escapes + +行の先頭に `!` を付けると、REPL から組み込みの `bash` ツール経由でローカルシェルコマンドを実行できます: + +```text +!pwd +!git status --short +``` + +IaC Code は通常のツール権限チェックを適用し、現在のプロジェクトコンテキストでコマンドを実行して、出力を端末に表示します。このコマンドはチャットメッセージとしてモデルには送信されません。 diff --git a/website/i18n/pt/docusaurus-plugin-content-docs/current/cli/interactive-mode.md b/website/i18n/pt/docusaurus-plugin-content-docs/current/cli/interactive-mode.md index 87fd494..a62a058 100644 --- a/website/i18n/pt/docusaurus-plugin-content-docs/current/cli/interactive-mode.md +++ b/website/i18n/pt/docusaurus-plugin-content-docs/current/cli/interactive-mode.md @@ -24,3 +24,20 @@ Em seguida, descreva o que deseja construir: ```text Create a VPC, two ECS instances, and a security group that allows SSH from my office IP. ``` + +## Editar entrada + +Use `Shift+Enter` para inserir uma nova linha sem enviar o prompt. Pressione `Enter` sozinho para enviar o prompt completo. + +Se o seu terminal nao informar `Shift+Enter` separadamente, pressione `Esc` e depois `Enter` para inserir uma nova linha. Prompts com varias linhas sao salvos como uma unica entrada de historico, entao `Up` restaura o prompt completo. + +## Shell escapes + +Prefixe uma linha com `!` para executar um comando shell local a partir do REPL por meio da ferramenta `bash` integrada: + +```text +!pwd +!git status --short +``` + +O IaC Code aplica as verificacoes normais de permissao de ferramentas, executa o comando no contexto do projeto atual e mostra a saida no terminal. O comando nao e enviado ao modelo como mensagem de chat. diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/cli/interactive-mode.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/cli/interactive-mode.md index 4b09b8d..e6b565a 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/cli/interactive-mode.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/cli/interactive-mode.md @@ -24,3 +24,20 @@ iac-code ```text 创建一个 VPC、两台 ECS 实例,以及一个允许办公 IP 通过 SSH 访问的安全组。 ``` + +## 编辑输入 + +使用 `Shift+Enter` 可以插入换行而不发送 prompt。单独按 `Enter` 会提交完整 prompt。 + +如果你的终端无法单独上报 `Shift+Enter`,可以先按 `Esc` 再按 `Enter` 来插入换行。多行 prompt 会作为一个完整的历史记录保存,因此按 `Up` 可以恢复完整 prompt。 + +## Shell escapes + +在一行开头输入 `!`,可以在 REPL 中通过内置 `bash` 工具运行本地 shell 命令: + +```text +!pwd +!git status --short +``` + +IaC Code 会应用正常的工具权限检查,在当前项目上下文中运行命令,并把输出显示在终端里。该命令不会作为聊天消息发送给模型。