From ab8c338882a93e13ecea3d99a9198b1cf55a168d Mon Sep 17 00:00:00 2001 From: aaron Date: Thu, 19 Feb 2026 08:26:33 +0800 Subject: [PATCH] feat: add CLI tip system - show helpful tips after commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds connectonion/cli/tips.py with 20 contextual tips covering major features: @xray debugging, co/ managed keys, event system, plugins, host()/connect() multi-agent, email/calendar, llm_do(), and more. Tips are shown after successful CLI commands (init, create, deploy, auth, keys, status, doctor, browser, ai, copy, eval), are contextual (filtered by command), non-repeating (tracked in ~/.co/tips_seen.json), and can be disabled via [cli] tips = false in ~/.co/config.toml. fixes #72 🧅 Built by OpenOnion - Open Source AI Automation https://github.com/openonion --- connectonion/cli/main.py | 22 ++++++ connectonion/cli/tips.py | 166 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 connectonion/cli/tips.py diff --git a/connectonion/cli/main.py b/connectonion/cli/main.py index 733bbeda..671779a1 100644 --- a/connectonion/cli/main.py +++ b/connectonion/cli/main.py @@ -85,7 +85,9 @@ def init( ): """Initialize project in current directory.""" from .commands.init import handle_init + from .tips import show_tip handle_init(ai=None, key=key, template=template, description=description, yes=yes, force=force) + show_tip("init") @app.command() @@ -98,19 +100,24 @@ def create( ): """Create new project.""" from .commands.create import handle_create + from .tips import show_tip handle_create(name=name, ai=None, key=key, template=template, description=description, yes=yes) + show_tip("create") @app.command() def deploy(): """Deploy to ConnectOnion Cloud.""" from .commands.deploy_commands import handle_deploy + from .tips import show_tip handle_deploy() + show_tip("deploy") @app.command() def auth(service: Optional[str] = typer.Argument(None, help="Service: google, microsoft")): """Authenticate with OpenOnion.""" + from .tips import show_tip if service == "google": from .commands.auth_commands import handle_google_auth handle_google_auth() @@ -120,6 +127,7 @@ def auth(service: Optional[str] = typer.Argument(None, help="Service: google, mi else: from .commands.auth_commands import handle_auth handle_auth() + show_tip("auth") @app.command() @@ -128,14 +136,18 @@ def keys( ): """Show agent keys and credentials.""" from .commands.keys_commands import handle_keys + from .tips import show_tip handle_keys(reveal=reveal) + show_tip("keys") @app.command() def status(): """Check account status.""" from .commands.status_commands import handle_status + from .tips import show_tip handle_status() + show_tip("status") @app.command() @@ -149,14 +161,18 @@ def reset(): def doctor(): """Diagnose installation.""" from .commands.doctor_commands import handle_doctor + from .tips import show_tip handle_doctor() + show_tip("doctor") @app.command() def browser(command: str = typer.Argument(..., help="Browser command")): """Browser automation.""" from .commands.browser_commands import handle_browser + from .tips import show_tip handle_browser(command) + show_tip("browser") @app.command() @@ -168,7 +184,9 @@ def ai( ): """Start AI coding agent or run one-shot prompt.""" from .commands.ai_commands import handle_ai + from .tips import show_tip handle_ai(prompt=prompt, port=port, model=model, max_iterations=max_iterations) + show_tip("ai") @app.command() @@ -180,7 +198,9 @@ def copy( ): """Copy built-in tools/plugins to customize.""" from .commands.copy_commands import handle_copy + from .tips import show_tip handle_copy(names=names or [], list_all=list_all, path=path, force=force) + show_tip("copy") @app.command() @@ -190,7 +210,9 @@ def eval( ): """Run evals and show results.""" from .commands.eval_commands import handle_eval + from .tips import show_tip handle_eval(name=name, agent_file=agent) + show_tip("eval") # Trust command group diff --git a/connectonion/cli/tips.py b/connectonion/cli/tips.py new file mode 100644 index 00000000..fe57f1d6 --- /dev/null +++ b/connectonion/cli/tips.py @@ -0,0 +1,166 @@ +import json +import random +from pathlib import Path +from rich.console import Console + +console = Console() + +TIPS = [ + { + "text": "Use @xray decorator on any tool to pause and inspect agent state during execution.", + "link": "docs.connectonion.com/xray", + "context": ["create", "init"], + }, + { + "text": 'Try model="co/gemini-2.5-pro" for free managed LLM access — no API key needed.', + "link": "docs.connectonion.com/models", + "context": ["auth", "status"], + }, + { + "text": "Add type hints to tool functions for better LLM schema generation.\n def search(query: str, limit: int = 10) -> list: ...", + "link": "docs.connectonion.com/tools", + "context": ["create", "init"], + }, + { + "text": "Use host() and connect() to build multi-agent systems that collaborate.\n from connectonion.network import host, connect", + "link": "docs.connectonion.com/network", + "context": ["create", "init", "deploy"], + }, + { + "text": "The event system lets you hook into agent lifecycle: after_llm, after_tools, on_complete.\n agent = Agent('name', on_events=[after_tools(my_handler)])", + "link": "docs.connectonion.com/events", + "context": ["create", "init"], + }, + { + "text": "Plugins bundle event handlers together — try the built-in reflection plugin.\n from connectonion.useful_plugins import reflection", + "link": "docs.connectonion.com/plugins", + "context": ["create", "init"], + }, + { + "text": "co browser launches a browser automation agent. Try: co -b 'find flights to Tokyo'", + "link": "docs.connectonion.com/browser", + "context": ["create", "status", "auth"], + }, + { + "text": "Use co/ prefix models for managed keys: model='co/gemini-2.5-flash' for fast, cheap tasks.", + "link": "docs.connectonion.com/models", + "context": ["status", "auth", "keys"], + }, + { + "text": "Join our Discord for help, tips, and to share your agents.\n discord.gg/4xfD9k8AUF", + "link": "discord.gg/4xfD9k8AUF", + "context": [], + }, + { + "text": "Add connect.py integrations for Gmail/Calendar with: co auth google", + "link": "docs.connectonion.com/email", + "context": ["auth", "create", "init"], + }, + { + "text": "Use quiet=True to suppress console output for background agents.\n agent = Agent('name', quiet=True)", + "link": "docs.connectonion.com/logging", + "context": ["create", "init"], + }, + { + "text": "llm_do() runs a one-shot LLM call without creating a full agent.\n from connectonion import llm_do", + "link": "docs.connectonion.com/llm-do", + "context": ["create", "init"], + }, + { + "text": "Use agent.receive_all() to iterate over streaming events from a remote agent.", + "link": "docs.connectonion.com/network", + "context": ["create", "deploy"], + }, + { + "text": "co copy copies built-in tool source to your project for customization.\n co copy send_email", + "link": "docs.connectonion.com/tools", + "context": ["create", "init", "copy"], + }, + { + "text": "Set max_iterations on your agent to control how long it runs.\n agent = Agent('name', max_iterations=50)", + "link": "docs.connectonion.com/agent", + "context": ["create", "init"], + }, + { + "text": "Use structured_complete() to get typed Pydantic output from LLMs.\n llm.structured_complete(messages, MySchema)", + "link": "docs.connectonion.com/llm", + "context": ["create", "init"], + }, + { + "text": "Sessions are saved to .co/evals/ as YAML for debugging and replaying.", + "link": "docs.connectonion.com/sessions", + "context": ["eval", "status"], + }, + { + "text": "co eval runs your eval files and shows pass/fail results.", + "link": "docs.connectonion.com/eval", + "context": ["eval"], + }, + { + "text": "Use trust='careful' in host() to require signature verification from remote agents.", + "link": "docs.connectonion.com/trust", + "context": ["deploy", "create"], + }, + { + "text": "Full docs at docs.connectonion.com — includes tutorials, API reference, and examples.", + "link": "docs.connectonion.com", + "context": [], + }, +] + +_TIPS_SEEN_FILE = Path.home() / ".co" / "tips_seen.json" +_CONFIG_FILE = Path.home() / ".co" / "config.toml" + + +def _tips_enabled() -> bool: + if not _CONFIG_FILE.exists(): + return True + import toml + config = toml.load(_CONFIG_FILE) + return config.get("cli", {}).get("tips", True) + + +def _load_seen() -> list: + if not _TIPS_SEEN_FILE.exists(): + return [] + return json.loads(_TIPS_SEEN_FILE.read_text()) + + +def _save_seen(seen: list): + _TIPS_SEEN_FILE.parent.mkdir(parents=True, exist_ok=True) + _TIPS_SEEN_FILE.write_text(json.dumps(seen)) + + +def show_tip(command: str): + if not _tips_enabled(): + return + + seen = _load_seen() + + # Find tips matching this command context, preferring unseen ones + contextual = [t for t in TIPS if command in t["context"]] + universal = [t for t in TIPS if not t["context"]] + candidates = contextual + universal + + # Filter unseen; if all seen, reset rotation + unseen = [t for t in candidates if t["text"] not in seen] + if not unseen: + # All seen - reset and show any + seen = [] + unseen = candidates + + if not unseen: + return + + tip = random.choice(unseen) + seen.append(tip["text"]) + + # Keep seen list bounded + if len(seen) > len(TIPS): + seen = seen[-len(TIPS):] + + _save_seen(seen) + + console.print() + console.print(f"[bold yellow]💡 Tip:[/bold yellow] {tip['text']}") + console.print(f" [dim]Learn more: {tip['link']}[/dim]")