diff --git a/src/aipass/cli/README.md b/src/aipass/cli/README.md index 3095422a..9889ee75 100644 --- a/src/aipass/cli/README.md +++ b/src/aipass/cli/README.md @@ -5,7 +5,7 @@ **Purpose:** Display and output formatting service for AIPass modules. Provides consistent terminal output — headers, success/error/warning messages, section breaks, and operation templates — so every module looks the same without duplicating Rich formatting code. **Module:** `aipass.cli` **Seedgo:** 100% -**Tests:** 151 passing (6 files, 5/5 modules covered) +**Tests:** 161 passing (6 files, 5/5 modules covered) **Last Updated:** 2026-04-11 ## Usage @@ -66,7 +66,7 @@ cli/ ├── cli_json/ # Auto-created JSON output (three-file pattern) ├── dropbox/ # Inbound file drop ├── logs/ # Branch-level logs -├── tests/ # 151 tests across 6 files +├── tests/ # 161 tests across 6 files │ ├── test_bootstrap.py # bootstrap.py handler tests │ ├── test_json_handler.py # json_handler tests │ ├── test_display.py # display module tests diff --git a/src/aipass/cli/apps/handlers/init/bootstrap.py b/src/aipass/cli/apps/handlers/init/bootstrap.py index 5fc20b6e..d90e64b4 100644 --- a/src/aipass/cli/apps/handlers/init/bootstrap.py +++ b/src/aipass/cli/apps/handlers/init/bootstrap.py @@ -412,9 +412,23 @@ def _gitignore() -> str: def _claude_settings(aipass_home: str | None = None) -> str: """Generate .claude/settings.json — minimal hooks for prompt injection. + Installs two UserPromptSubmit hooks: + 1. Global prompt — injects .aipass/aipass_global_prompt.md from CWD. + 2. Local prompt — walks up from CWD to find .aipass/aipass_local_prompt.md + (branch-level prompt, e.g. inside src//). + Args: aipass_home: Optional AIPass installation root to add as env.AIPASS_HOME. """ + _local_prompt_cmd = ( + "dir=$(pwd); " + "while [ \"$dir\" != \"/\" ]; do " + "if [ -f \"$dir/.aipass/aipass_local_prompt.md\" ]; then " + "cat \"$dir/.aipass/aipass_local_prompt.md\"; break; " + "fi; " + "dir=$(dirname \"$dir\"); " + "done" + ) data: dict = { "hooks": { "UserPromptSubmit": [ @@ -426,7 +440,16 @@ def _claude_settings(aipass_home: str | None = None) -> str: "command": "cat .aipass/aipass_global_prompt.md 2>/dev/null || true", } ], - } + }, + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": _local_prompt_cmd, + } + ], + }, ] } } diff --git a/src/aipass/cli/apps/modules/init_project.py b/src/aipass/cli/apps/modules/init_project.py index 0729800e..11be5785 100644 --- a/src/aipass/cli/apps/modules/init_project.py +++ b/src/aipass/cli/apps/modules/init_project.py @@ -260,6 +260,12 @@ def _handle_init(args: List[str]) -> bool: success(f"Created {len(result['created_files'])} files") + if result.get("aipass_home"): + console.print() + console.print(f"[bold cyan]AIPASS_HOME:[/bold cyan] [yellow]{result['aipass_home']}[/yellow]") + console.print(f"[dim]For terminal usage, add to your shell profile:[/dim]") + console.print(f" [green]export AIPASS_HOME={result['aipass_home']}[/green]") + json_handler.log_operation("aipass_init", { "project_name": result["project_name"], "target": result["target"], @@ -462,7 +468,7 @@ def _print_init_help(): console.print("[bold cyan]WHAT IT CREATES:[/bold cyan]") console.print() - files_text = """[bold]Project scaffold (11 items):[/bold] + files_text = """[bold]Project scaffold (12 items):[/bold] [green]1.[/green] [yellow]{NAME}_REGISTRY.json[/yellow] Project registry with UUID [green]2.[/green] [yellow].aipass/aipass_global_prompt.md[/yellow] Global prompt (injected every turn) @@ -472,9 +478,10 @@ def _print_init_help(): [green]6.[/green] [yellow]README.md[/yellow] Getting started guide [green]7.[/green] [yellow]STATUS.local.md[/yellow] Project status [green]8.[/green] [yellow].gitignore[/yellow] Standard AIPass ignores - [green]9.[/green] [yellow].claude/settings.json[/yellow] Claude Code hooks + [green]9.[/green] [yellow].claude/settings.json[/yellow] Claude Code hooks + AIPASS_HOME [green]10.[/green] [yellow]hooks/[/yellow] User hooks directory - [green]11.[/green] [yellow]src/[/yellow] Agent directories live here""" + [green]11.[/green] [yellow]src/[/yellow] Agent directories live here + [green]12.[/green] [yellow].ai_mail.local/inbox.json[/yellow] Empty project mailbox""" console.print(Panel(files_text, border_style="green", padding=(1, 2), box=box.ROUNDED)) console.print() diff --git a/src/aipass/cli/tests/test_bootstrap.py b/src/aipass/cli/tests/test_bootstrap.py index b0a413a4..effb11ce 100644 --- a/src/aipass/cli/tests/test_bootstrap.py +++ b/src/aipass/cli/tests/test_bootstrap.py @@ -278,6 +278,28 @@ def test_init_project_claude_settings_content(tmp_path): assert "UserPromptSubmit" in data["hooks"] +def test_init_project_settings_has_two_hooks(tmp_path): + """.claude/settings.json has both global and local prompt hooks.""" + target = tmp_path / "proj" + target.mkdir() + + init_project(target, project_name="alpha") + + settings_path = target / ".claude" / "settings.json" + data = json.loads(settings_path.read_text(encoding="utf-8")) + hooks = data["hooks"]["UserPromptSubmit"] + assert len(hooks) == 2, f"Expected 2 UserPromptSubmit hooks, got {len(hooks)}" + + # First hook: global prompt + global_cmd = hooks[0]["hooks"][0]["command"] + assert "aipass_global_prompt.md" in global_cmd + + # Second hook: local prompt walk-up + local_cmd = hooks[1]["hooks"][0]["command"] + assert "aipass_local_prompt.md" in local_cmd + assert "dirname" in local_cmd, "Local prompt hook should walk up with dirname" + + def test_init_project_global_prompt_content(tmp_path): """Global prompt contains project name and AIPass terminology.""" target = tmp_path / "proj"